From 94327b83112d9e5c22366aa2538839d6ac6e04bd Mon Sep 17 00:00:00 2001 From: Manish Gupta <59428681+mguptahub@users.noreply.github.com> Date: Fri, 8 Mar 2024 15:16:32 +0530 Subject: [PATCH 01/24] chore: feature build process optimization (#3907) * process changed to build tar and use for deployment * fixes --- .github/workflows/feature-deployment.yml | 186 +++++++++++++++++++---- 1 file changed, 155 insertions(+), 31 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 7b9f5ffcc..12549cff5 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -4,70 +4,194 @@ on: workflow_dispatch: inputs: web-build: - required: true + required: false + description: 'Build Web' type: boolean default: true space-build: - required: true + required: false + description: 'Build Space' type: boolean default: false +env: + BUILD_WEB: ${{ github.event.inputs.web-build }} + BUILD_SPACE: ${{ github.event.inputs.space-build }} + jobs: + setup-feature-build: + name: Feature Build Setup + runs-on: ubuntu-latest + steps: + - name: Checkout + run: | + echo "BUILD_WEB=$BUILD_WEB" + echo "BUILD_SPACE=$BUILD_SPACE" + outputs: + web-build: ${{ env.BUILD_WEB}} + space-build: ${{env.BUILD_SPACE}} + + feature-build-web: + if: ${{ needs.setup-feature-build.outputs.web-build == 'true' }} + needs: setup-feature-build + name: Feature Build Web + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: feature-preview + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn install + - name: Build Web + id: build-web + run: | + cd $GITHUB_WORKSPACE/feature-preview + yarn build --filter=web + cd $GITHUB_WORKSPACE + + TAR_NAME="web.tar.gz" + tar -czf $TAR_NAME ./feature-preview + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + + feature-build-space: + if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }} + needs: setup-feature-build + name: Feature Build Space + runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + outputs: + do-build: ${{ needs.setup-feature-build.outputs.space-build }} + s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} + steps: + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli + - name: Checkout + uses: actions/checkout@v4 + with: + path: plane + - name: Install Dependencies + run: | + cd $GITHUB_WORKSPACE/plane + yarn install + - name: Build Space + id: build-space + run: | + cd $GITHUB_WORKSPACE/plane + yarn build --filter=space + cd $GITHUB_WORKSPACE + + TAR_NAME="space.tar.gz" + tar -czf $TAR_NAME ./plane + + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") + aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY + feature-deploy: + if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true') }} + needs: [feature-build-web, feature-build-space] name: Feature Deploy runs-on: ubuntu-latest env: - KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG }} - BUILD_WEB: ${{ (github.event.inputs.web-build == '' && true) || github.event.inputs.web-build }} - BUILD_SPACE: ${{ (github.event.inputs.space-build == '' && false) || github.event.inputs.space-build }} - + AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} + AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} + KUBE_CONFIG_FILE: ${{ secrets.FEATURE_PREVIEW_KUBE_CONFIG }} steps: + - name: Install AWS cli + run: | + sudo apt-get update + sudo apt-get install -y python3-pip + pip3 install awscli - name: Tailscale uses: tailscale/github-action@v2 with: oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }} oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }} tags: tag:ci - - name: Kubectl Setup run: | - curl -LO "https://dl.k8s.io/release/${{secrets.KUBE_VERSION}}/bin/linux/amd64/kubectl" + curl -LO "https://dl.k8s.io/release/${{ vars.FEATURE_PREVIEW_KUBE_VERSION }}/bin/linux/amd64/kubectl" chmod +x kubectl mkdir -p ~/.kube echo "$KUBE_CONFIG_FILE" > ~/.kube/config chmod 600 ~/.kube/config - - name: HELM Setup run: | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 chmod 700 get_helm.sh ./get_helm.sh - - name: App Deploy run: | - helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ secrets.FEATURE_PREVIEW_HELM_CHART_URL }} - GIT_BRANCH=${{ github.ref_name }} - APP_NAMESPACE=${{ secrets.FEATURE_PREVIEW_NAMESPACE }} + WEB_S3_URL="" + if [ ${{ env.BUILD_WEB }} == true ]; then + WEB_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/web.tar.gz --expires-in 3600) + fi - METADATA=$(helm install feature-preview/${{ secrets.FEATURE_PREVIEW_HELM_CHART_NAME }} \ - --kube-insecure-skip-tls-verify \ - --generate-name \ - --namespace $APP_NAMESPACE \ - --set shared_config.git_repo=${{github.server_url}}/${{ github.repository }}.git \ - --set shared_config.git_branch="$GIT_BRANCH" \ - --set web.enabled=${{ env.BUILD_WEB }} \ - --set space.enabled=${{ env.BUILD_SPACE }} \ - --output json \ - --timeout 1000s) + SPACE_S3_URL="" + if [ ${{ env.BUILD_SPACE }} == true ]; then + SPACE_S3_URL=$(aws s3 presign s3://${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}/${{github.sha}}/space.tar.gz --expires-in 3600) + fi - APP_NAME=$(echo $METADATA | jq -r '.name') + if [ ${{ env.BUILD_WEB }} == true ] || [ ${{ env.BUILD_SPACE }} == true ]; then - INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ - -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ - jq -r '.spec.rules[0].host') + helm --kube-insecure-skip-tls-verify repo add feature-preview ${{ vars.FEATURE_PREVIEW_HELM_CHART_URL }} - echo "****************************************" - echo "APP NAME ::: $APP_NAME" - echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" - echo "****************************************" + APP_NAMESPACE="${{ vars.FEATURE_PREVIEW_NAMESPACE }}" + DEPLOY_SCRIPT_URL="${{ vars.FEATURE_PREVIEW_DEPLOY_SCRIPT_URL }}" + + METADATA=$(helm --kube-insecure-skip-tls-verify install feature-preview/${{ vars.FEATURE_PREVIEW_HELM_CHART_NAME }} \ + --generate-name \ + --namespace $APP_NAMESPACE \ + --set ingress.primaryDomain=${{vars.FEATURE_PREVIEW_PRIMARY_DOMAIN || 'feature.plane.tools' }} \ + --set web.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set web.enabled=${{ env.BUILD_WEB || false }} \ + --set web.artifact_url=$WEB_S3_URL \ + --set space.image=${{vars.FEATURE_PREVIEW_DOCKER_BASE}} \ + --set space.enabled=${{ env.BUILD_SPACE || false }} \ + --set space.artifact_url=$SPACE_S3_URL \ + --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --output json \ + --timeout 1000s) + + APP_NAME=$(echo $METADATA | jq -r '.name') + + INGRESS_HOSTNAME=$(kubectl get ingress -n feature-builds --insecure-skip-tls-verify \ + -o jsonpath='{.items[?(@.metadata.annotations.meta\.helm\.sh\/release-name=="'$APP_NAME'")]}' | \ + jq -r '.spec.rules[0].host') + + echo "****************************************" + echo "APP NAME ::: $APP_NAME" + echo "INGRESS HOSTNAME ::: $INGRESS_HOSTNAME" + echo "****************************************" + fi From 8cc372679c526c096b144af1b9a2b990b1e0db2b Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 15:56:05 +0530 Subject: [PATCH 02/24] Web Build fixes --- .github/workflows/feature-deployment.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 12549cff5..766f30514 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -40,7 +40,7 @@ jobs: AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} - NEXT_PUBLIC_API_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} steps: - name: Set up Node.js uses: actions/setup-node@v4 @@ -54,21 +54,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: feature-preview + path: plane - name: Install Dependencies run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn install - name: Build Web id: build-web run: | - cd $GITHUB_WORKSPACE/feature-preview + cd $GITHUB_WORKSPACE/plane yarn build --filter=web cd $GITHUB_WORKSPACE TAR_NAME="web.tar.gz" - tar -czf $TAR_NAME ./feature-preview - + tar -czf $TAR_NAME ./plane + FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY @@ -82,6 +82,7 @@ jobs: AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 + NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} outputs: do-build: ${{ needs.setup-feature-build.outputs.space-build }} s3-url: ${{ steps.build-space.outputs.S3_PRESIGNED_URL }} From 6c6b7156bbb46fa5d2ebbd4e0d851b264d5a2cd3 Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Fri, 8 Mar 2024 16:00:18 +0530 Subject: [PATCH 03/24] helm variable update --- .github/workflows/feature-deployment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/feature-deployment.yml b/.github/workflows/feature-deployment.yml index 766f30514..c5eec3cd3 100644 --- a/.github/workflows/feature-deployment.yml +++ b/.github/workflows/feature-deployment.yml @@ -182,6 +182,7 @@ jobs: --set space.enabled=${{ env.BUILD_SPACE || false }} \ --set space.artifact_url=$SPACE_S3_URL \ --set shared_config.deploy_script_url=$DEPLOY_SCRIPT_URL \ + --set shared_config.api_base_url=${{vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL}} \ --output json \ --timeout 1000s) From cb78ccad1f9f0d9266dda5c7fc365a59d562a5d1 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Fri, 8 Mar 2024 16:58:37 +0530 Subject: [PATCH 04/24] fix: 1click deployment fixes --- deploy/1-click/plane-app | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/deploy/1-click/plane-app b/deploy/1-click/plane-app index e6bd24b9e..ace0a0b79 100644 --- a/deploy/1-click/plane-app +++ b/deploy/1-click/plane-app @@ -494,13 +494,6 @@ function install() { update_env "CORS_ALLOWED_ORIGINS" "http://$MY_IP" update_config "INSTALLATION_DATE" "$(date '+%Y-%m-%d')" - - if command -v crontab &> /dev/null; then - sudo touch /etc/cron.daily/makeplane - sudo chmod +x /etc/cron.daily/makeplane - sudo echo "0 2 * * * root /usr/local/bin/plane-app --upgrade" > /etc/cron.daily/makeplane - sudo crontab /etc/cron.daily/makeplane - fi show_message "Plane Installed Successfully ✅" show_message "" @@ -606,11 +599,6 @@ function uninstall() { sudo rm $PLANE_INSTALL_DIR/variables-upgrade.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/config.env &> /dev/null sudo rm $PLANE_INSTALL_DIR/docker-compose.yaml &> /dev/null - - if command -v crontab &> /dev/null; then - sudo crontab -r &> /dev/null - sudo rm /etc/cron.daily/makeplane &> /dev/null - fi # rm -rf $PLANE_INSTALL_DIR &> /dev/null show_message "- Configuration Cleaned ✅" From 8997ee2e3e741b03a4e55cfd0ab6eee22c3a356b Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Mon, 11 Mar 2024 20:45:51 +0530 Subject: [PATCH 05/24] fix: eslint fixes --- packages/ui/.eslintrc.js | 4 ---- packages/ui/package.json | 1 - web/components/inbox/modals/create-issue-modal.tsx | 1 - .../issue-layouts/filters/header/filters/state-group.tsx | 1 - 4 files changed, 7 deletions(-) delete mode 100644 packages/ui/.eslintrc.js diff --git a/packages/ui/.eslintrc.js b/packages/ui/.eslintrc.js deleted file mode 100644 index c8df60750..000000000 --- a/packages/ui/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ["custom"], -}; diff --git a/packages/ui/package.json b/packages/ui/package.json index f80bcc6ae..fdd67dcc1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -14,7 +14,6 @@ "scripts": { "build": "tsup src/index.ts --format esm,cjs --dts --external react --minify", "dev": "tsup src/index.ts --format esm,cjs --watch --dts --external react", - "lint": "eslint src/", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "dependencies": { diff --git a/web/components/inbox/modals/create-issue-modal.tsx b/web/components/inbox/modals/create-issue-modal.tsx index 2603b712e..549af7229 100644 --- a/web/components/inbox/modals/create-issue-modal.tsx +++ b/web/components/inbox/modals/create-issue-modal.tsx @@ -73,7 +73,6 @@ export const CreateInboxIssueModal: React.FC = observer((props) => { reset, watch, getValues, - setValue, } = useForm({ defaultValues }); const handleClose = () => { diff --git a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx index e283112be..ca7fe270d 100644 --- a/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx +++ b/web/components/issues/issue-layouts/filters/header/filters/state-group.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; -import sortBy from "lodash/sortBy"; // components import { StateGroupIcon } from "@plane/ui"; import { FilterHeader, FilterOption } from "components/issues"; From 899771a6789579a7f4d6c98ec391b9dcb052c5b5 Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:55:24 +0530 Subject: [PATCH 06/24] [WEB-434] feat: add support to insert a new empty line on clicking at bottom of the editor (#3856) * fix: horizontal rule no more causes issues on last node * fixed the mismatched transaction by using native tiptap stuff * added support to add new line onclick at bottom if last node is an image TODO: blockquote at last node * fix: simplified adding node at end of the document logic * feat: rewrite entire logic handling all cases * feat: arrow down and arrow up keys add empty node at top and bottom of doc from first/last row's cells * feat: added arrow up and down key support to images too * remove unnecessary console logs * chore: formatting components * fix: reduced bottom padding to increase onclick area --------- Co-authored-by: sriram veeraghanta --- packages/editor/core/src/lib/utils.ts | 14 ++++ .../src/ui/components/editor-container.tsx | 67 ++++++++++++++----- .../core/src/ui/extensions/image/index.tsx | 8 +++ .../utilities/insert-line-above-image.ts | 45 +++++++++++++ .../utilities/insert-line-below-image.ts | 46 +++++++++++++ .../src/ui/extensions/table/table/table.ts | 4 ++ .../insert-line-above-table-action.ts | 50 ++++++++++++++ .../insert-line-below-table-action.ts | 48 +++++++++++++ .../src/ui/components/page-renderer.tsx | 2 +- .../editor/rich-text-editor/src/ui/index.tsx | 8 +-- .../src/ui/menus/bubble-menu/index.tsx | 5 +- .../ui/menus/bubble-menu/link-selector.tsx | 12 +++- .../ui/menus/bubble-menu/node-selector.tsx | 8 ++- 13 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts create mode 100644 packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts create mode 100644 packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts diff --git a/packages/editor/core/src/lib/utils.ts b/packages/editor/core/src/lib/utils.ts index 5c7a8f08f..c943d4c60 100644 --- a/packages/editor/core/src/lib/utils.ts +++ b/packages/editor/core/src/lib/utils.ts @@ -1,3 +1,4 @@ +import { Selection } from "@tiptap/pm/state"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; interface EditorClassNames { @@ -18,6 +19,19 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +// Helper function to find the parent node of a specific type +export function findParentNodeOfType(selection: Selection, typeName: string) { + let depth = selection.$anchor.depth; + while (depth > 0) { + const node = selection.$anchor.node(depth); + if (node.type.name === typeName) { + return { node, pos: selection.$anchor.start(depth) - 1 }; + } + depth--; + } + return null; +} + export const findTableAncestor = (node: Node | null): HTMLTableElement | null => { while (node !== null && node.nodeName !== "TABLE") { node = node.parentNode; diff --git a/packages/editor/core/src/ui/components/editor-container.tsx b/packages/editor/core/src/ui/components/editor-container.tsx index 5480a51e9..1b2504b58 100644 --- a/packages/editor/core/src/ui/components/editor-container.tsx +++ b/packages/editor/core/src/ui/components/editor-container.tsx @@ -1,5 +1,5 @@ import { Editor } from "@tiptap/react"; -import { ReactNode } from "react"; +import { FC, ReactNode } from "react"; interface EditorContainerProps { editor: Editor | null; @@ -8,17 +8,54 @@ interface EditorContainerProps { hideDragHandle?: () => void; } -export const EditorContainer = ({ editor, editorClassNames, hideDragHandle, children }: EditorContainerProps) => ( -
{ - editor?.chain().focus(undefined, { scrollIntoView: false }).run(); - }} - onMouseLeave={() => { - hideDragHandle?.(); - }} - className={`cursor-text ${editorClassNames}`} - > - {children} -
-); +export const EditorContainer: FC = (props) => { + const { editor, editorClassNames, hideDragHandle, children } = props; + + const handleContainerClick = () => { + if (!editor) return; + if (!editor.isEditable) return; + if (editor.isFocused) return; // If editor is already focused, do nothing + + const { selection } = editor.state; + const currentNode = selection.$from.node(); + + editor?.chain().focus("end", { scrollIntoView: false }).run(); // Focus the editor at the end + + if ( + currentNode.content.size === 0 && // Check if the current node is empty + !( + editor.isActive("orderedList") || + editor.isActive("bulletList") || + editor.isActive("taskItem") || + editor.isActive("table") || + editor.isActive("blockquote") || + editor.isActive("codeBlock") + ) // Check if it's an empty node within an orderedList, bulletList, taskItem, table, quote or code block + ) { + return; + } + + // Insert a new paragraph at the end of the document + const endPosition = editor?.state.doc.content.size; + editor?.chain().insertContentAt(endPosition, { type: "paragraph" }).run(); + + // Focus the newly added paragraph for immediate editing + editor + .chain() + .setTextSelection(endPosition + 1) + .run(); + }; + + return ( +
{ + hideDragHandle?.(); + }} + className={`cursor-text ${editorClassNames}`} + > + {children} +
+ ); +}; diff --git a/packages/editor/core/src/ui/extensions/image/index.tsx b/packages/editor/core/src/ui/extensions/image/index.tsx index db8b1c97b..1431b7755 100644 --- a/packages/editor/core/src/ui/extensions/image/index.tsx +++ b/packages/editor/core/src/ui/extensions/image/index.tsx @@ -5,6 +5,8 @@ import ImageExt from "@tiptap/extension-image"; import { onNodeDeleted, onNodeRestored } from "src/ui/plugins/delete-image"; import { DeleteImage } from "src/types/delete-image"; import { RestoreImage } from "src/types/restore-image"; +import { insertLineBelowImageAction } from "./utilities/insert-line-below-image"; +import { insertLineAboveImageAction } from "./utilities/insert-line-above-image"; interface ImageNode extends ProseMirrorNode { attrs: { @@ -18,6 +20,12 @@ const IMAGE_NODE_TYPE = "image"; export const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) => ImageExt.extend({ + addKeyboardShortcuts() { + return { + ArrowDown: insertLineBelowImageAction, + ArrowUp: insertLineAboveImageAction, + }; + }, addProseMirrorPlugins() { return [ UploadImagesPlugin(cancelUploadImage), diff --git a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts new file mode 100644 index 000000000..a18576b46 --- /dev/null +++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-above-image.ts @@ -0,0 +1,45 @@ +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { KeyboardShortcutCommand } from "@tiptap/core"; + +export const insertLineAboveImageAction: KeyboardShortcutCommand = ({ editor }) => { + const { selection, doc } = editor.state; + const { $from, $to } = selection; + + let imageNode: ProseMirrorNode | null = null; + let imagePos: number | null = null; + + // Check if the selection itself is an image node + doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === "image") { + imageNode = node; + imagePos = pos; + return false; // Stop iterating once an image node is found + } + return true; + }); + + if (imageNode === null || imagePos === null) return false; + + // Since we want to insert above the image, we use the imagePos directly + const insertPos = imagePos; + + if (insertPos < 0) return false; + + // Check for an existing node immediately before the image + if (insertPos === 0) { + // If the previous node doesn't exist or isn't a paragraph, create and insert a new empty node there + editor.chain().insertContentAt(insertPos, { type: "paragraph" }).run(); + editor.chain().setTextSelection(insertPos).run(); + } else { + const prevNode = doc.nodeAt(insertPos); + + if (prevNode && prevNode.type.name === "paragraph") { + // If the previous node is a paragraph, move the cursor there + editor.chain().setTextSelection(insertPos).run(); + } else { + return false; + } + } + + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts new file mode 100644 index 000000000..e998c728b --- /dev/null +++ b/packages/editor/core/src/ui/extensions/image/utilities/insert-line-below-image.ts @@ -0,0 +1,46 @@ +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { KeyboardShortcutCommand } from "@tiptap/core"; + +export const insertLineBelowImageAction: KeyboardShortcutCommand = ({ editor }) => { + const { selection, doc } = editor.state; + const { $from, $to } = selection; + + let imageNode: ProseMirrorNode | null = null; + let imagePos: number | null = null; + + // Check if the selection itself is an image node + doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.type.name === "image") { + imageNode = node; + imagePos = pos; + return false; // Stop iterating once an image node is found + } + return true; + }); + + if (imageNode === null || imagePos === null) return false; + + const guaranteedImageNode: ProseMirrorNode = imageNode; + const nextNodePos = imagePos + guaranteedImageNode.nodeSize; + + // Check for an existing node immediately after the image + const nextNode = doc.nodeAt(nextNodePos); + + if (nextNode && nextNode.type.name === "paragraph") { + // If the next node is a paragraph, move the cursor there + const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else if (!nextNode) { + // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there + editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(nextNodePos + 1) + .run(); + } else { + // If the next node is not a paragraph, do not proceed + return false; + } + + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/table/table/table.ts b/packages/editor/core/src/ui/extensions/table/table/table.ts index ef595eee2..5fd06caf6 100644 --- a/packages/editor/core/src/ui/extensions/table/table/table.ts +++ b/packages/editor/core/src/ui/extensions/table/table/table.ts @@ -25,6 +25,8 @@ import { tableControls } from "src/ui/extensions/table/table/table-controls"; import { TableView } from "src/ui/extensions/table/table/table-view"; import { createTable } from "src/ui/extensions/table/table/utilities/create-table"; import { deleteTableWhenAllCellsSelected } from "src/ui/extensions/table/table/utilities/delete-table-when-all-cells-selected"; +import { insertLineBelowTableAction } from "./utilities/insert-line-below-table-action"; +import { insertLineAboveTableAction } from "./utilities/insert-line-above-table-action"; export interface TableOptions { HTMLAttributes: Record; @@ -231,6 +233,8 @@ export const Table = Node.create({ "Mod-Backspace": deleteTableWhenAllCellsSelected, Delete: deleteTableWhenAllCellsSelected, "Mod-Delete": deleteTableWhenAllCellsSelected, + ArrowDown: insertLineBelowTableAction, + ArrowUp: insertLineAboveTableAction, }; }, diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts new file mode 100644 index 000000000..d61d21c5b --- /dev/null +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-above-table-action.ts @@ -0,0 +1,50 @@ +import { KeyboardShortcutCommand } from "@tiptap/core"; +import { findParentNodeOfType } from "src/lib/utils"; + +export const insertLineAboveTableAction: KeyboardShortcutCommand = ({ editor }) => { + // Check if the current selection or the closest node is a table + if (!editor.isActive("table")) return false; + + // Get the current selection + const { selection } = editor.state; + + // Find the table node and its position + const tableNode = findParentNodeOfType(selection, "table"); + if (!tableNode) return false; + + const tablePos = tableNode.pos; + + // Determine if the selection is in the first row of the table + const firstRow = tableNode.node.child(0); + const selectionPath = (selection.$anchor as any).path; + const selectionInFirstRow = selectionPath.includes(firstRow); + + if (!selectionInFirstRow) return false; + + // Check if the table is at the very start of the document or its parent node + if (tablePos === 0) { + // The table is at the start, so just insert a paragraph at the current position + editor.chain().insertContentAt(tablePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(tablePos + 1) + .run(); + } else { + // The table is not at the start, check for the node immediately before the table + const prevNodePos = tablePos - 1; + + if (prevNodePos <= 0) return false; + + const prevNode = editor.state.doc.nodeAt(prevNodePos - 1); + + if (prevNode && prevNode.type.name === "paragraph") { + // If there's a paragraph before the table, move the cursor to the end of that paragraph + const endOfParagraphPos = tablePos - prevNode.nodeSize; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else { + return false; + } + } + + return true; +}; diff --git a/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts new file mode 100644 index 000000000..28b46084a --- /dev/null +++ b/packages/editor/core/src/ui/extensions/table/table/utilities/insert-line-below-table-action.ts @@ -0,0 +1,48 @@ +import { KeyboardShortcutCommand } from "@tiptap/core"; +import { findParentNodeOfType } from "src/lib/utils"; + +export const insertLineBelowTableAction: KeyboardShortcutCommand = ({ editor }) => { + // Check if the current selection or the closest node is a table + if (!editor.isActive("table")) return false; + + // Get the current selection + const { selection } = editor.state; + + // Find the table node and its position + const tableNode = findParentNodeOfType(selection, "table"); + if (!tableNode) return false; + + const tablePos = tableNode.pos; + const table = tableNode.node; + + // Determine if the selection is in the last row of the table + const rowCount = table.childCount; + const lastRow = table.child(rowCount - 1); + const selectionPath = (selection.$anchor as any).path; + const selectionInLastRow = selectionPath.includes(lastRow); + + if (!selectionInLastRow) return false; + + // Calculate the position immediately after the table + const nextNodePos = tablePos + table.nodeSize; + + // Check for an existing node immediately after the table + const nextNode = editor.state.doc.nodeAt(nextNodePos); + + if (nextNode && nextNode.type.name === "paragraph") { + // If the next node is an paragraph, move the cursor there + const endOfParagraphPos = nextNodePos + nextNode.nodeSize - 1; + editor.chain().setTextSelection(endOfParagraphPos).run(); + } else if (!nextNode) { + // If the next node doesn't exist i.e. we're at the end of the document, create and insert a new empty node there + editor.chain().insertContentAt(nextNodePos, { type: "paragraph" }).run(); + editor + .chain() + .setTextSelection(nextNodePos + 1) + .run(); + } else { + return false; + } + + return true; +}; diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx index 06b9e70ff..d82719c87 100644 --- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx +++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx @@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => { ); return ( -
+
{!readonly ? ( handlePageTitleChange(e.target.value)} diff --git a/packages/editor/rich-text-editor/src/ui/index.tsx b/packages/editor/rich-text-editor/src/ui/index.tsx index 2aff5d265..eeac3d2ef 100644 --- a/packages/editor/rich-text-editor/src/ui/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/index.tsx @@ -100,10 +100,10 @@ const RichTextEditor = ({ customClassName, }); - React.useEffect(() => { - if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue); - }, [editor, initialValue]); - + // React.useEffect(() => { + // if (editor && initialValue && editor.getHTML() != initialValue) editor.commands.setContent(initialValue); + // }, [editor, initialValue]); + // if (!editor) return null; return ( diff --git a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx index 2e7dd25b8..f96e7293e 100644 --- a/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx +++ b/packages/editor/rich-text-editor/src/ui/menus/bubble-menu/index.tsx @@ -121,7 +121,10 @@ export const EditorBubbleMenu: FC = (props: any) => { +

+
+
+ + + ); + } const endDate = new Date(activeCycle.end_date ?? ""); const startDate = new Date(activeCycle.start_date ?? ""); + const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0; + const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups; const groupedIssues: any = { backlog: activeCycle.backlog_issues, @@ -94,8 +134,6 @@ export const ActiveCycleDetails: React.FC = observer((props cancelled: activeCycle.cancelled_issues, }; - const cycleStatus = activeCycle.status.toLowerCase() as TCycleGroups; - const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -148,8 +186,6 @@ export const ActiveCycleDetails: React.FC = observer((props color: group.color, })); - const daysLeft = findHowManyDaysLeft(activeCycle.end_date) ?? 0; - return (
@@ -203,27 +239,15 @@ export const ActiveCycleDetails: React.FC = observer((props
- {cycleOwnerDetails?.avatar && cycleOwnerDetails?.avatar !== "" ? ( - {cycleOwnerDetails?.display_name} - ) : ( - - {cycleOwnerDetails?.display_name.charAt(0)} - - )} + {cycleOwnerDetails?.display_name}
{activeCycle.assignee_ids.length > 0 && (
- {activeCycle.assignee_ids.map((assigne_id) => { - const member = getUserDetails(assigne_id); + {activeCycle.assignee_ids.map((assignee_id) => { + const member = getUserDetails(assignee_id); return ; })} @@ -233,7 +257,7 @@ export const ActiveCycleDetails: React.FC = observer((props
- + {activeCycle.total_issues} issues
@@ -244,9 +268,9 @@ export const ActiveCycleDetails: React.FC = observer((props - View Cycle + View cycle
@@ -287,11 +311,11 @@ export const ActiveCycleDetails: React.FC = observer((props
-
High Priority Issues
+
High priority issues
{activeCycleIssues ? ( activeCycleIssues.length > 0 ? ( - activeCycleIssues.map((issue: any) => ( + activeCycleIssues.map((issue) => ( = observer((props
{}} - projectId={projectId?.toString() ?? ""} + projectId={projectId} disabled buttonVariant="background-with-text" /> @@ -359,10 +383,10 @@ export const ActiveCycleDetails: React.FC = observer((props
- + - Pending Issues -{" "} + Pending issues-{" "} {activeCycle.total_issues - (activeCycle.completed_issues + activeCycle.cancelled_issues)}
diff --git a/web/components/cycles/active-cycle-stats.tsx b/web/components/cycles/active-cycle/stats.tsx similarity index 98% rename from web/components/cycles/active-cycle-stats.tsx rename to web/components/cycles/active-cycle/stats.tsx index 0cf7449ae..9ccd11077 100644 --- a/web/components/cycles/active-cycle-stats.tsx +++ b/web/components/cycles/active-cycle/stats.tsx @@ -134,7 +134,7 @@ export const ActiveCycleProgressStats: React.FC = ({ cycle }) => { ) : (
- There are no high priority issues present in this cycle. + There are no issues present in this cycle.
)} diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx new file mode 100644 index 000000000..af2b02726 --- /dev/null +++ b/web/components/cycles/active-cycle/upcoming-cycles-list-item.tsx @@ -0,0 +1,135 @@ +import Link from "next/link"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +import { Star, User2 } from "lucide-react"; +// hooks +import { useCycle, useEventTracker, useMember } from "hooks/store"; +// components +import { CycleQuickActions } from "components/cycles"; +// ui +import { Avatar, AvatarGroup, setPromiseToast } from "@plane/ui"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +// constants +import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; + +type Props = { + cycleId: string; +}; + +export const UpcomingCycleListItem: React.FC = observer((props) => { + const { cycleId } = props; + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + // store hooks + const { captureEvent } = useEventTracker(); + const { addCycleToFavorites, getCycleById, removeCycleFromFavorites } = useCycle(); + const { getUserDetails } = useMember(); + // derived values + const cycle = getCycleById(cycleId); + + const handleAddToFavorites = (e: React.MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; + + const addToFavoritePromise = addCycleToFavorites(workspaceSlug?.toString(), projectId.toString(), cycleId).then( + () => { + captureEvent(CYCLE_FAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", + }); + } + ); + + setPromiseToast(addToFavoritePromise, { + loading: "Adding cycle to favorites...", + success: { + title: "Success!", + message: () => "Cycle added to favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't add the cycle to favorites. Please try again.", + }, + }); + }; + + const handleRemoveFromFavorites = (e: React.MouseEvent) => { + e.preventDefault(); + if (!workspaceSlug || !projectId) return; + + const removeFromFavoritePromise = removeCycleFromFavorites( + workspaceSlug?.toString(), + projectId.toString(), + cycleId + ).then(() => { + captureEvent(CYCLE_UNFAVORITED, { + cycle_id: cycleId, + element: "List layout", + state: "SUCCESS", + }); + }); + + setPromiseToast(removeFromFavoritePromise, { + loading: "Removing cycle from favorites...", + success: { + title: "Success!", + message: () => "Cycle removed from favorites.", + }, + error: { + title: "Error!", + message: () => "Couldn't remove the cycle from favorites. Please try again.", + }, + }); + }; + + if (!cycle) return null; + + return ( + +
{cycle.name}
+
+ {cycle.start_date && cycle.end_date && ( +
+ {renderFormattedDate(cycle.start_date)} - {renderFormattedDate(cycle.end_date)} +
+ )} + {cycle.assignee_ids?.length > 0 ? ( + + {cycle.assignee_ids?.map((assigneeId) => { + const member = getUserDetails(assigneeId); + return ; + })} + + ) : ( + + + + )} + + {cycle.is_favorite ? ( + + ) : ( + + )} + + {workspaceSlug && projectId && ( + + )} +
+ + ); +}); diff --git a/web/components/cycles/active-cycle/upcoming-cycles-list.tsx b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx new file mode 100644 index 000000000..60fa9bb30 --- /dev/null +++ b/web/components/cycles/active-cycle/upcoming-cycles-list.tsx @@ -0,0 +1,25 @@ +import { observer } from "mobx-react"; +// hooks +import { useCycle } from "hooks/store"; +// components +import { UpcomingCycleListItem } from "components/cycles"; + +export const UpcomingCyclesList = observer(() => { + // store hooks + const { currentProjectUpcomingCycleIds } = useCycle(); + + if (!currentProjectUpcomingCycleIds) return null; + + return ( +
+
+ Upcoming cycles +
+
+ {currentProjectUpcomingCycleIds.map((cycleId) => ( + + ))} +
+
+ ); +}); diff --git a/web/components/cycles/applied-filters/date.tsx b/web/components/cycles/applied-filters/date.tsx new file mode 100644 index 000000000..0298f12d2 --- /dev/null +++ b/web/components/cycles/applied-filters/date.tsx @@ -0,0 +1,55 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// helpers +import { renderFormattedDate } from "helpers/date-time.helper"; +import { capitalizeFirstLetter } from "helpers/string.helper"; +// constants +import { DATE_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + editable: boolean | undefined; + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedDateFilters: React.FC = observer((props) => { + const { editable, handleRemove, values } = props; + + const getDateLabel = (value: string): string => { + let dateLabel = ""; + + const dateDetails = DATE_FILTER_OPTIONS.find((d) => d.value === value); + + if (dateDetails) dateLabel = dateDetails.name; + else { + const dateParts = value.split(";"); + + if (dateParts.length === 2) { + const [date, time] = dateParts; + + dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`; + } + } + + return dateLabel; + }; + + return ( + <> + {values.map((date) => ( +
+ {getDateLabel(date)} + {editable && ( + + )} +
+ ))} + + ); +}); diff --git a/web/components/cycles/applied-filters/index.ts b/web/components/cycles/applied-filters/index.ts new file mode 100644 index 000000000..cee9ae349 --- /dev/null +++ b/web/components/cycles/applied-filters/index.ts @@ -0,0 +1,3 @@ +export * from "./date"; +export * from "./root"; +export * from "./status"; diff --git a/web/components/cycles/applied-filters/root.tsx b/web/components/cycles/applied-filters/root.tsx new file mode 100644 index 000000000..39d2ae827 --- /dev/null +++ b/web/components/cycles/applied-filters/root.tsx @@ -0,0 +1,90 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +// hooks +import { useUser } from "hooks/store"; +// components +import { AppliedDateFilters, AppliedStatusFilters } from "components/cycles"; +// helpers +import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; +// types +import { TCycleFilters } from "@plane/types"; +// constants +import { EUserProjectRoles } from "constants/project"; + +type Props = { + appliedFilters: TCycleFilters; + handleClearAllFilters: () => void; + handleRemoveFilter: (key: keyof TCycleFilters, value: string | null) => void; + alwaysAllowEditing?: boolean; +}; + +const DATE_FILTERS = ["start_date", "end_date"]; + +export const CycleAppliedFiltersList: React.FC = observer((props) => { + const { appliedFilters, handleClearAllFilters, handleRemoveFilter, alwaysAllowEditing } = props; + // store hooks + const { + membership: { currentProjectRole }, + } = useUser(); + + if (!appliedFilters) return null; + + if (Object.keys(appliedFilters).length === 0) return null; + + const isEditingAllowed = alwaysAllowEditing || (currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER); + + return ( +
+ {Object.entries(appliedFilters).map(([key, value]) => { + const filterKey = key as keyof TCycleFilters; + + if (!value) return; + if (Array.isArray(value) && value.length === 0) return; + + return ( +
+ {replaceUnderscoreIfSnakeCase(filterKey)} +
+ {filterKey === "status" && ( + handleRemoveFilter("status", val)} + values={value} + /> + )} + {DATE_FILTERS.includes(filterKey) && ( + handleRemoveFilter(filterKey, val)} + values={value} + /> + )} + {isEditingAllowed && ( + + )} +
+
+ ); + })} + {isEditingAllowed && ( + + )} +
+ ); +}); diff --git a/web/components/cycles/applied-filters/status.tsx b/web/components/cycles/applied-filters/status.tsx new file mode 100644 index 000000000..1eb28db74 --- /dev/null +++ b/web/components/cycles/applied-filters/status.tsx @@ -0,0 +1,43 @@ +import { observer } from "mobx-react-lite"; +import { X } from "lucide-react"; +import { CYCLE_STATUS } from "constants/cycle"; +import { cn } from "helpers/common.helper"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedStatusFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + + return ( + <> + {values.map((status) => { + const statusDetails = CYCLE_STATUS.find((s) => s.value === status); + return ( +
+ {statusDetails?.title} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/cycles/cycles-board-card.tsx b/web/components/cycles/board/cycles-board-card.tsx similarity index 71% rename from web/components/cycles/cycles-board-card.tsx rename to web/components/cycles/board/cycles-board-card.tsx index da97f2d9d..ac95f790d 100644 --- a/web/components/cycles/cycles-board-card.tsx +++ b/web/components/cycles/board/cycles-board-card.tsx @@ -1,22 +1,12 @@ -import { FC, MouseEvent, useState } from "react"; +import { FC, MouseEvent } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks // components -import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react"; -import { - Avatar, - AvatarGroup, - CustomMenu, - Tooltip, - LayersIcon, - CycleGroupIcon, - TOAST_TYPE, - setToast, - setPromiseToast, -} from "@plane/ui"; -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { Info, Star } from "lucide-react"; +import { Avatar, AvatarGroup, Tooltip, LayersIcon, CycleGroupIcon, setPromiseToast } from "@plane/ui"; +import { CycleQuickActions } from "components/cycles"; // ui // icons // helpers @@ -24,7 +14,6 @@ import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; -import { copyTextToClipboard } from "helpers/string.helper"; // constants import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; //.types @@ -38,13 +27,10 @@ export interface ICyclesBoardCard { export const CyclesBoardCard: FC = observer((props) => { const { cycleId, workspaceSlug, projectId } = props; - // states - const [updateModal, setUpdateModal] = useState(false); - const [deleteModal, setDeleteModal] = useState(false); // router const router = useRouter(); // store - const { setTrackElement, captureEvent } = useEventTracker(); + const { captureEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); @@ -56,7 +42,6 @@ export const CyclesBoardCard: FC = observer((props) => { if (!cycleDetails) return null; const cycleStatus = cycleDetails.status.toLocaleLowerCase(); - const isCompleted = cycleStatus === "completed"; const endDate = new Date(cycleDetails.end_date ?? ""); const startDate = new Date(cycleDetails.start_date ?? ""); const isDateValid = cycleDetails.start_date || cycleDetails.end_date; @@ -78,24 +63,10 @@ export const CyclesBoardCard: FC = observer((props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycleDetails.completed_issues - ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; - const handleCopyText = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link Copied!", - message: "Cycle link copied to clipboard.", - }); - }); - }; - const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -152,20 +123,6 @@ export const CyclesBoardCard: FC = observer((props) => { }); }; - const handleEditCycle = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setTrackElement("Cycles page grid layout"); - setUpdateModal(true); - }; - - const handleDeleteCycle = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setTrackElement("Cycles page grid layout"); - setDeleteModal(true); - }; - const openCycleOverview = (e: MouseEvent) => { const { query } = router; e.preventDefault(); @@ -181,22 +138,6 @@ export const CyclesBoardCard: FC = observer((props) => { return (
- setUpdateModal(false)} - workspaceSlug={workspaceSlug} - projectId={projectId} - /> - - setDeleteModal(false)} - workspaceSlug={workspaceSlug} - projectId={projectId} - /> -
@@ -288,30 +229,8 @@ export const CyclesBoardCard: FC = observer((props) => { ))} - - {!isCompleted && isEditingAllowed && ( - <> - - - - Edit cycle - - - - - - Delete cycle - - - - )} - - - - Copy cycle link - - - + +
diff --git a/web/components/cycles/board/cycles-board-map.tsx b/web/components/cycles/board/cycles-board-map.tsx new file mode 100644 index 000000000..4218c0d1c --- /dev/null +++ b/web/components/cycles/board/cycles-board-map.tsx @@ -0,0 +1,25 @@ +// components +import { CyclesBoardCard } from "components/cycles"; + +type Props = { + cycleIds: string[]; + peekCycle: string | undefined; + projectId: string; + workspaceSlug: string; +}; + +export const CyclesBoardMap: React.FC = (props) => { + const { cycleIds, peekCycle, projectId, workspaceSlug } = props; + + return ( +
+ {cycleIds.map((cycleId) => ( + + ))} +
+ ); +}; diff --git a/web/components/cycles/board/index.ts b/web/components/cycles/board/index.ts new file mode 100644 index 000000000..2e6933d99 --- /dev/null +++ b/web/components/cycles/board/index.ts @@ -0,0 +1,3 @@ +export * from "./cycles-board-card"; +export * from "./cycles-board-map"; +export * from "./root"; diff --git a/web/components/cycles/board/root.tsx b/web/components/cycles/board/root.tsx new file mode 100644 index 000000000..26154becf --- /dev/null +++ b/web/components/cycles/board/root.tsx @@ -0,0 +1,60 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import { Disclosure } from "@headlessui/react"; +import { ChevronRight } from "lucide-react"; +// components +import { CyclePeekOverview, CyclesBoardMap } from "components/cycles"; +// helpers +import { cn } from "helpers/common.helper"; + +export interface ICyclesBoard { + completedCycleIds: string[]; + cycleIds: string[]; + workspaceSlug: string; + projectId: string; + peekCycle: string | undefined; +} + +export const CyclesBoard: FC = observer((props) => { + const { completedCycleIds, cycleIds, workspaceSlug, projectId, peekCycle } = props; + + return ( +
+
+
+ + {completedCycleIds.length !== 0 && ( + + + {({ open }) => ( + <> + Completed cycles ({completedCycleIds.length}) + + + )} + + + + + + )} +
+ +
+
+ ); +}); diff --git a/web/components/cycles/cycles-board.tsx b/web/components/cycles/cycles-board.tsx deleted file mode 100644 index 278d55071..000000000 --- a/web/components/cycles/cycles-board.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { CyclePeekOverview, CyclesBoardCard } from "components/cycles"; -import { EmptyState } from "components/empty-state"; -// constants -import { EMPTY_STATE_DETAILS } from "constants/empty-state"; - -export interface ICyclesBoard { - cycleIds: string[]; - filter: string; - workspaceSlug: string; - projectId: string; - peekCycle: string | undefined; -} - -export const CyclesBoard: FC = observer((props) => { - const { cycleIds, filter, workspaceSlug, projectId, peekCycle } = props; - - return ( - <> - {cycleIds?.length > 0 ? ( -
-
-
- {cycleIds.map((cycleId) => ( - - ))} -
- -
-
- ) : ( - - )} - - ); -}); diff --git a/web/components/cycles/cycles-list.tsx b/web/components/cycles/cycles-list.tsx deleted file mode 100644 index f6ad64f99..000000000 --- a/web/components/cycles/cycles-list.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { FC } from "react"; -import { observer } from "mobx-react-lite"; -// components -import { CyclePeekOverview, CyclesListItem } from "components/cycles"; -import { EmptyState } from "components/empty-state"; -// ui -import { Loader } from "@plane/ui"; -// constants -import { EMPTY_STATE_DETAILS } from "constants/empty-state"; - -export interface ICyclesList { - cycleIds: string[]; - filter: string; - workspaceSlug: string; - projectId: string; -} - -export const CyclesList: FC = observer((props) => { - const { cycleIds, filter, workspaceSlug, projectId } = props; - - return ( - <> - {cycleIds ? ( - <> - {cycleIds.length > 0 ? ( -
-
-
- {cycleIds.map((cycleId) => ( - - ))} -
- -
-
- ) : ( - - )} - - ) : ( - - - - - - )} - - ); -}); diff --git a/web/components/cycles/cycles-view-header.tsx b/web/components/cycles/cycles-view-header.tsx new file mode 100644 index 000000000..b0feede0e --- /dev/null +++ b/web/components/cycles/cycles-view-header.tsx @@ -0,0 +1,164 @@ +import { useCallback, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { Tab } from "@headlessui/react"; +import { ListFilter, Search, X } from "lucide-react"; +// hooks +import { useCycleFilter } from "hooks/store"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// components +import { CycleFiltersSelection } from "components/cycles"; +import { FiltersDropdown } from "components/issues"; +// ui +import { Tooltip } from "@plane/ui"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { TCycleFilters } from "@plane/types"; +// constants +import { CYCLE_TABS_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; + +type Props = { + projectId: string; +}; + +export const CyclesViewHeader: React.FC = observer((props) => { + const { projectId } = props; + // states + const [isSearchOpen, setIsSearchOpen] = useState(false); + // refs + const inputRef = useRef(null); + // hooks + const { + currentProjectDisplayFilters, + currentProjectFilters, + searchQuery, + updateDisplayFilters, + updateFilters, + updateSearchQuery, + } = useCycleFilter(); + // outside click detector hook + useOutsideClickDetector(inputRef, () => { + if (isSearchOpen && searchQuery.trim() === "") setIsSearchOpen(false); + }); + + const handleFilters = useCallback( + (key: keyof TCycleFilters, value: string | string[]) => { + const newValues = currentProjectFilters?.[key] ?? []; + + if (Array.isArray(value)) + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + else { + if (currentProjectFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(projectId, { [key]: newValues }); + }, + [currentProjectFilters, projectId, updateFilters] + ); + + const handleInputKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + if (searchQuery && searchQuery.trim() !== "") updateSearchQuery(""); + else setIsSearchOpen(false); + } + }; + + return ( +
+ + {CYCLE_TABS_LIST.map((tab) => ( + + `border-b-2 p-4 text-sm font-medium outline-none ${ + selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" + }` + } + > + {tab.name} + + ))} + + {currentProjectDisplayFilters?.active_tab !== "active" && ( +
+ {!isSearchOpen && ( + + )} +
+ + updateSearchQuery(e.target.value)} + onKeyDown={handleInputKeyDown} + /> + {isSearchOpen && ( + + )} +
+ } title="Filters" placement="bottom-end"> + + +
+ {CYCLE_VIEW_LAYOUTS.map((layout) => ( + + + + ))} +
+
+ )} +
+ ); +}); diff --git a/web/components/cycles/cycles-view.tsx b/web/components/cycles/cycles-view.tsx index 745ca1bd3..447bd048c 100644 --- a/web/components/cycles/cycles-view.tsx +++ b/web/components/cycles/cycles-view.tsx @@ -1,43 +1,35 @@ import { FC } from "react"; +import Image from "next/image"; import { observer } from "mobx-react-lite"; // hooks +import { useCycle, useCycleFilter } from "hooks/store"; // components import { CyclesBoard, CyclesList, CyclesListGanttChartView } from "components/cycles"; -// ui components +// ui import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; -import { useCycle } from "hooks/store"; +// assets +import NameFilterImage from "public/empty-state/cycle/name-filter.svg"; +import AllFiltersImage from "public/empty-state/cycle/all-filters.svg"; // types -import { TCycleLayout, TCycleView } from "@plane/types"; +import { TCycleLayoutOptions } from "@plane/types"; export interface ICyclesView { - filter: TCycleView; - layout: TCycleLayout; + layout: TCycleLayoutOptions; workspaceSlug: string; projectId: string; peekCycle: string | undefined; } export const CyclesView: FC = observer((props) => { - const { filter, layout, workspaceSlug, projectId, peekCycle } = props; + const { layout, workspaceSlug, projectId, peekCycle } = props; // store hooks - const { - currentProjectCompletedCycleIds, - currentProjectDraftCycleIds, - currentProjectUpcomingCycleIds, - currentProjectCycleIds, - loader, - } = useCycle(); + const { getFilteredCycleIds, getFilteredCompletedCycleIds, loader } = useCycle(); + const { searchQuery } = useCycleFilter(); + // derived values + const filteredCycleIds = getFilteredCycleIds(projectId); + const filteredCompletedCycleIds = getFilteredCompletedCycleIds(projectId); - const cyclesList = - filter === "completed" - ? currentProjectCompletedCycleIds - : filter === "draft" - ? currentProjectDraftCycleIds - : filter === "upcoming" - ? currentProjectUpcomingCycleIds - : currentProjectCycleIds; - - if (loader || !cyclesList) + if (loader || !filteredCycleIds) return ( <> {layout === "list" && } @@ -46,23 +38,45 @@ export const CyclesView: FC = observer((props) => { ); + if (filteredCycleIds.length === 0 && filteredCompletedCycleIds?.length === 0) + return ( +
+
+ No matching cycles +
No matching cycles
+

+ {searchQuery.trim() === "" + ? "Remove the filters to see all cycles" + : "Remove the search criteria to see all cycles"} +

+
+
+ ); + return ( <> {layout === "list" && ( - + )} - {layout === "board" && ( )} - - {layout === "gantt" && } + {layout === "gantt" && } ); }); diff --git a/web/components/cycles/delete-modal.tsx b/web/components/cycles/delete-modal.tsx index fd7b1f356..0d1cc5921 100644 --- a/web/components/cycles/delete-modal.tsx +++ b/web/components/cycles/delete-modal.tsx @@ -103,7 +103,7 @@ export const CycleDeleteModal: React.FC = observer((props) => {
-
Delete Cycle
+
Delete cycle

@@ -118,8 +118,8 @@ export const CycleDeleteModal: React.FC = observer((props) => { Cancel -

diff --git a/web/components/cycles/dropdowns/filters/end-date.tsx b/web/components/cycles/dropdowns/filters/end-date.tsx new file mode 100644 index 000000000..10a401500 --- /dev/null +++ b/web/components/cycles/dropdowns/filters/end-date.tsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; + +// components +import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; +// constants +import { DATE_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterEndDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Due date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple + /> + ))} + setIsDateFilterModalOpen(true)} title="Custom" multiple /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/cycles/dropdowns/filters/index.ts b/web/components/cycles/dropdowns/filters/index.ts new file mode 100644 index 000000000..3d097b6f0 --- /dev/null +++ b/web/components/cycles/dropdowns/filters/index.ts @@ -0,0 +1,4 @@ +export * from "./end-date"; +export * from "./root"; +export * from "./start-date"; +export * from "./status"; diff --git a/web/components/cycles/dropdowns/filters/root.tsx b/web/components/cycles/dropdowns/filters/root.tsx new file mode 100644 index 000000000..d97fcad03 --- /dev/null +++ b/web/components/cycles/dropdowns/filters/root.tsx @@ -0,0 +1,69 @@ +import { useState } from "react"; +import { observer } from "mobx-react-lite"; +import { Search, X } from "lucide-react"; +// components +import { FilterEndDate, FilterStartDate, FilterStatus } from "components/cycles"; +// types +import { TCycleFilters, TCycleGroups } from "@plane/types"; + +type Props = { + filters: TCycleFilters; + handleFiltersUpdate: (key: keyof TCycleFilters, value: string | string[]) => void; +}; + +export const CycleFiltersSelection: React.FC = observer((props) => { + const { filters, handleFiltersUpdate } = props; + // states + const [filtersSearchQuery, setFiltersSearchQuery] = useState(""); + + return ( +
+
+
+ + setFiltersSearchQuery(e.target.value)} + autoFocus + /> + {filtersSearchQuery !== "" && ( + + )} +
+
+
+ {/* cycle status */} +
+ handleFiltersUpdate("status", val)} + searchQuery={filtersSearchQuery} + /> +
+ + {/* start date */} +
+ handleFiltersUpdate("start_date", val)} + searchQuery={filtersSearchQuery} + /> +
+ + {/* end date */} +
+ handleFiltersUpdate("end_date", val)} + searchQuery={filtersSearchQuery} + /> +
+
+
+ ); +}); diff --git a/web/components/cycles/dropdowns/filters/start-date.tsx b/web/components/cycles/dropdowns/filters/start-date.tsx new file mode 100644 index 000000000..87def7e29 --- /dev/null +++ b/web/components/cycles/dropdowns/filters/start-date.tsx @@ -0,0 +1,63 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; + +// components +import { DateFilterModal } from "components/core"; +import { FilterHeader, FilterOption } from "components/issues"; +// constants +import { DATE_FILTER_OPTIONS } from "constants/filters"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterStartDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_FILTER_OPTIONS.filter((d) => d.name.toLowerCase().includes(searchQuery.toLowerCase())); + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Start date" + /> + )} + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + <> + {filteredOptions.map((option) => ( + handleUpdate(option.value)} + title={option.name} + multiple + /> + ))} + setIsDateFilterModalOpen(true)} title="Custom" multiple /> + + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/cycles/dropdowns/filters/status.tsx b/web/components/cycles/dropdowns/filters/status.tsx new file mode 100644 index 000000000..79e53a5c8 --- /dev/null +++ b/web/components/cycles/dropdowns/filters/status.tsx @@ -0,0 +1,49 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react-lite"; +// components +import { FilterHeader, FilterOption } from "components/issues"; +// types +import { TCycleGroups } from "@plane/types"; +// constants +import { CYCLE_STATUS } from "constants/cycle"; + +type Props = { + appliedFilters: TCycleGroups[] | null; + handleUpdate: (val: string) => void; + searchQuery: string; +}; + +export const FilterStatus: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + // states + const [previewEnabled, setPreviewEnabled] = useState(true); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + const filteredOptions = CYCLE_STATUS.filter((p) => p.value.includes(searchQuery.toLowerCase())); + + return ( + <> + 0 ? ` (${appliedFiltersCount})` : ""}`} + isPreviewEnabled={previewEnabled} + handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} + /> + {previewEnabled && ( +
+ {filteredOptions.length > 0 ? ( + filteredOptions.map((status) => ( + handleUpdate(status.value)} + title={status.title} + /> + )) + ) : ( +

No matches found

+ )} +
+ )} + + ); +}); diff --git a/web/components/cycles/dropdowns/index.ts b/web/components/cycles/dropdowns/index.ts new file mode 100644 index 000000000..302e3a1a6 --- /dev/null +++ b/web/components/cycles/dropdowns/index.ts @@ -0,0 +1 @@ +export * from "./filters"; diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 521273c51..094fbea7b 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -4,8 +4,7 @@ import { useRouter } from "next/router"; // hooks import { CycleGanttBlock } from "components/cycles"; import { GanttChartRoot, IBlockUpdateData, CycleGanttSidebar } from "components/gantt-chart"; -import { EUserProjectRoles } from "constants/project"; -import { useCycle, useUser } from "hooks/store"; +import { useCycle } from "hooks/store"; // components // types import { ICycle } from "@plane/types"; @@ -22,9 +21,6 @@ export const CyclesListGanttChartView: FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; // store hooks - const { - membership: { currentProjectRole }, - } = useUser(); const { getCycleById, updateCycleDetails } = useCycle(); const handleCycleUpdate = async (cycle: ICycle, data: IBlockUpdateData) => { @@ -52,9 +48,6 @@ export const CyclesListGanttChartView: FC = observer((props) => { return structuredBlocks; }; - const isAllowed = - currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - return (
= observer((props) => { enableBlockLeftResize={false} enableBlockRightResize={false} enableBlockMove={false} - enableReorder={isAllowed} + enableReorder={false} />
); diff --git a/web/components/cycles/index.ts b/web/components/cycles/index.ts index db5e9de9e..e37d266b7 100644 --- a/web/components/cycles/index.ts +++ b/web/components/cycles/index.ts @@ -1,17 +1,16 @@ -export * from "./cycles-view"; -export * from "./active-cycle-details"; -export * from "./active-cycle-stats"; +export * from "./active-cycle"; +export * from "./applied-filters"; +export * from "./board/"; +export * from "./dropdowns"; export * from "./gantt-chart"; +export * from "./list"; +export * from "./cycle-peek-overview"; +export * from "./cycles-view-header"; export * from "./cycles-view"; +export * from "./delete-modal"; export * from "./form"; export * from "./modal"; +export * from "./quick-actions"; export * from "./sidebar"; export * from "./transfer-issues-modal"; export * from "./transfer-issues"; -export * from "./cycles-list"; -export * from "./cycles-list-item"; -export * from "./cycles-board"; -export * from "./cycles-board-card"; -export * from "./delete-modal"; -export * from "./cycle-peek-overview"; -export * from "./cycles-list-item"; diff --git a/web/components/cycles/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx similarity index 71% rename from web/components/cycles/cycles-list-item.tsx rename to web/components/cycles/list/cycles-list-item.tsx index 9bf1866ff..90c6d5d02 100644 --- a/web/components/cycles/cycles-list-item.tsx +++ b/web/components/cycles/list/cycles-list-item.tsx @@ -1,26 +1,14 @@ -import { FC, MouseEvent, useState } from "react"; +import { FC, MouseEvent } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; // hooks -import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react"; -import { - CustomMenu, - Tooltip, - CircularProgressIndicator, - CycleGroupIcon, - AvatarGroup, - Avatar, - TOAST_TYPE, - setToast, - setPromiseToast, -} from "@plane/ui"; -import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +import { Check, Info, Star, User2 } from "lucide-react"; +import { Tooltip, CircularProgressIndicator, CycleGroupIcon, AvatarGroup, Avatar, setPromiseToast } from "@plane/ui"; +import { CycleQuickActions } from "components/cycles"; import { CYCLE_STATUS } from "constants/cycle"; import { CYCLE_FAVORITED, CYCLE_UNFAVORITED } from "constants/event-tracker"; -import { EUserWorkspaceRoles } from "constants/workspace"; import { findHowManyDaysLeft, renderFormattedDate } from "helpers/date-time.helper"; -import { copyTextToClipboard } from "helpers/string.helper"; import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; // components // ui @@ -29,6 +17,7 @@ import { useEventTracker, useCycle, useUser, useMember } from "hooks/store"; // constants // types import { TCycleGroups } from "@plane/types"; +import { EUserProjectRoles } from "constants/project"; type TCyclesListItem = { cycleId: string; @@ -42,33 +31,16 @@ type TCyclesListItem = { export const CyclesListItem: FC = observer((props) => { const { cycleId, workspaceSlug, projectId } = props; - // states - const [updateModal, setUpdateModal] = useState(false); - const [deleteModal, setDeleteModal] = useState(false); // router const router = useRouter(); // store hooks - const { setTrackElement, captureEvent } = useEventTracker(); + const { captureEvent } = useEventTracker(); const { membership: { currentProjectRole }, } = useUser(); const { getCycleById, addCycleToFavorites, removeCycleFromFavorites } = useCycle(); const { getUserDetails } = useMember(); - const handleCopyText = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : ""; - - copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Link Copied!", - message: "Cycle link copied to clipboard.", - }); - }); - }; - const handleAddToFavorites = (e: MouseEvent) => { e.preventDefault(); if (!workspaceSlug || !projectId) return; @@ -125,20 +97,6 @@ export const CyclesListItem: FC = observer((props) => { }); }; - const handleEditCycle = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setTrackElement("Cycles page list layout"); - setUpdateModal(true); - }; - - const handleDeleteCycle = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - setTrackElement("Cycles page list layout"); - setDeleteModal(true); - }; - const openCycleOverview = (e: MouseEvent) => { const { query } = router; e.preventDefault(); @@ -161,7 +119,7 @@ export const CyclesListItem: FC = observer((props) => { const endDate = new Date(cycleDetails.end_date ?? ""); const startDate = new Date(cycleDetails.start_date ?? ""); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; const cycleTotalIssues = cycleDetails.backlog_issues + @@ -184,20 +142,6 @@ export const CyclesListItem: FC = observer((props) => { return ( <> - setUpdateModal(false)} - workspaceSlug={workspaceSlug} - projectId={projectId} - /> - setDeleteModal(false)} - workspaceSlug={workspaceSlug} - projectId={projectId} - />
@@ -246,7 +190,7 @@ export const CyclesListItem: FC = observer((props) => {
)}
-
+
{renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`}
@@ -256,8 +200,8 @@ export const CyclesListItem: FC = observer((props) => {
{cycleDetails.assignee_ids?.length > 0 ? ( - {cycleDetails.assignee_ids?.map((assigne_id) => { - const member = getUserDetails(assigne_id); + {cycleDetails.assignee_ids?.map((assignee_id) => { + const member = getUserDetails(assignee_id); return ; })} @@ -281,30 +225,7 @@ export const CyclesListItem: FC = observer((props) => { )} - - {!isCompleted && isEditingAllowed && ( - <> - - - - Edit cycle - - - - - - Delete cycle - - - - )} - - - - Copy cycle link - - - + )}
diff --git a/web/components/cycles/list/cycles-list-map.tsx b/web/components/cycles/list/cycles-list-map.tsx new file mode 100644 index 000000000..c07b204b1 --- /dev/null +++ b/web/components/cycles/list/cycles-list-map.tsx @@ -0,0 +1,20 @@ +// components +import { CyclesListItem } from "components/cycles"; + +type Props = { + cycleIds: string[]; + projectId: string; + workspaceSlug: string; +}; + +export const CyclesListMap: React.FC = (props) => { + const { cycleIds, projectId, workspaceSlug } = props; + + return ( + <> + {cycleIds.map((cycleId) => ( + + ))} + + ); +}; diff --git a/web/components/cycles/list/index.ts b/web/components/cycles/list/index.ts new file mode 100644 index 000000000..46a3557d7 --- /dev/null +++ b/web/components/cycles/list/index.ts @@ -0,0 +1,3 @@ +export * from "./cycles-list-item"; +export * from "./cycles-list-map"; +export * from "./root"; diff --git a/web/components/cycles/list/root.tsx b/web/components/cycles/list/root.tsx new file mode 100644 index 000000000..27488d238 --- /dev/null +++ b/web/components/cycles/list/root.tsx @@ -0,0 +1,49 @@ +import { FC } from "react"; +import { observer } from "mobx-react-lite"; +import { Disclosure } from "@headlessui/react"; +import { ChevronRight } from "lucide-react"; +// components +import { CyclePeekOverview, CyclesListMap } from "components/cycles"; +// helpers +import { cn } from "helpers/common.helper"; + +export interface ICyclesList { + completedCycleIds: string[]; + cycleIds: string[]; + workspaceSlug: string; + projectId: string; +} + +export const CyclesList: FC = observer((props) => { + const { completedCycleIds, cycleIds, workspaceSlug, projectId } = props; + + return ( +
+
+
+ + {completedCycleIds.length !== 0 && ( + + + {({ open }) => ( + <> + Completed cycles ({completedCycleIds.length}) + + + )} + + + + + + )} +
+ +
+
+ ); +}); diff --git a/web/components/cycles/modal.tsx b/web/components/cycles/modal.tsx index 2d1640ec9..3f57fc204 100644 --- a/web/components/cycles/modal.tsx +++ b/web/components/cycles/modal.tsx @@ -11,7 +11,7 @@ import { CycleService } from "services/cycle.service"; // components // ui // types -import type { CycleDateCheckData, ICycle, TCycleView } from "@plane/types"; +import type { CycleDateCheckData, ICycle, TCycleTabOptions } from "@plane/types"; // constants type CycleModalProps = { @@ -34,7 +34,7 @@ export const CycleCreateUpdateModal: React.FC = (props) => { const { workspaceProjectIds } = useProject(); const { createCycle, updateCycleDetails } = useCycle(); - const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); + const { setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); const handleCreateCycle = async (payload: Partial) => { if (!workspaceSlug || !projectId) return; diff --git a/web/components/cycles/quick-actions.tsx b/web/components/cycles/quick-actions.tsx new file mode 100644 index 000000000..f1c930ccb --- /dev/null +++ b/web/components/cycles/quick-actions.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import { LinkIcon, Pencil, Trash2 } from "lucide-react"; +// hooks +import { useCycle, useEventTracker, useUser } from "hooks/store"; +// components +import { CycleCreateUpdateModal, CycleDeleteModal } from "components/cycles"; +// ui +import { CustomMenu, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { copyUrlToClipboard } from "helpers/string.helper"; +// constants +import { EUserProjectRoles } from "constants/project"; + +type Props = { + cycleId: string; + projectId: string; + workspaceSlug: string; +}; + +export const CycleQuickActions: React.FC = observer((props) => { + const { cycleId, projectId, workspaceSlug } = props; + // states + const [updateModal, setUpdateModal] = useState(false); + const [deleteModal, setDeleteModal] = useState(false); + // store hooks + const { setTrackElement } = useEventTracker(); + const { + membership: { currentWorkspaceAllProjectsRole }, + } = useUser(); + const { getCycleById } = useCycle(); + // derived values + const cycleDetails = getCycleById(cycleId); + const isCompleted = cycleDetails?.status.toLowerCase() === "completed"; + // auth + const isEditingAllowed = + !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER; + + const handleCopyText = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/cycles/${cycleId}`).then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Link Copied!", + message: "Cycle link copied to clipboard.", + }); + }); + }; + + const handleEditCycle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setTrackElement("Cycles page list layout"); + setUpdateModal(true); + }; + + const handleDeleteCycle = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setTrackElement("Cycles page list layout"); + setDeleteModal(true); + }; + + return ( + <> + {cycleDetails && ( +
+ setUpdateModal(false)} + workspaceSlug={workspaceSlug} + projectId={projectId} + /> + setDeleteModal(false)} + workspaceSlug={workspaceSlug} + projectId={projectId} + /> +
+ )} + + {!isCompleted && isEditingAllowed && ( + <> + + + + Edit cycle + + + + + + Delete cycle + + + + )} + + + + Copy cycle link + + + + + ); +}); diff --git a/web/components/gantt-chart/chart/header.tsx b/web/components/gantt-chart/chart/header.tsx index fe35c9e52..b4dcd6a62 100644 --- a/web/components/gantt-chart/chart/header.tsx +++ b/web/components/gantt-chart/chart/header.tsx @@ -25,7 +25,7 @@ export const GanttChartHeader: React.FC = observer((props) => { const { currentView } = useGanttChart(); return ( -
+
{title}
{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}
diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index 22637147f..6f019f3bd 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -13,7 +13,7 @@ import { CYCLE_VIEW_LAYOUTS } from "constants/cycle"; import { EUserProjectRoles } from "constants/project"; import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; -import { TCycleLayout } from "@plane/types"; +import { TCycleLayoutOptions } from "@plane/types"; import { ProjectLogo } from "components/project"; export const CyclesHeader: FC = observer(() => { @@ -33,10 +33,10 @@ export const CyclesHeader: FC = observer(() => { const canUserCreateCycle = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); + const { setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); const handleCurrentLayout = useCallback( - (_layout: TCycleLayout) => { + (_layout: TCycleLayoutOptions) => { setCycleLayout(_layout); }, [setCycleLayout] @@ -109,7 +109,7 @@ export const CyclesHeader: FC = observer(() => { key={layout.key} onClick={() => { // handleLayoutChange(ISSUE_LAYOUTS[index].key); - handleCurrentLayout(layout.key as TCycleLayout); + handleCurrentLayout(layout.key as TCycleLayoutOptions); }} className="flex items-center gap-2" > diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 10ad265f3..467258273 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -55,6 +55,7 @@ export const AppliedFiltersList: React.FC = observer((props) => { const filterKey = key as keyof IIssueFilterOptions; if (!value) return; + if (Array.isArray(value) && value.length === 0) return; return (
= (props) => { - const { children, title = "Dropdown", placement, disabled = false, tabIndex, menuButton } = props; + const { children, icon, title = "Dropdown", placement, disabled = false, tabIndex, menuButton } = props; const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); @@ -44,6 +45,7 @@ export const FiltersDropdown: React.FC = (props) => { ref={setReferenceElement} variant="neutral-primary" size="sm" + prependIcon={icon} appendIcon={ } @@ -64,9 +66,9 @@ export const FiltersDropdown: React.FC = (props) => { leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - +
{ + if (cycles.length === 0) return []; + + const STATUS_ORDER: { + [key: string]: number; + } = { + current: 1, + upcoming: 2, + draft: 3, + }; + + let filteredCycles = cycles.filter((c) => c.status.toLowerCase() !== "completed"); + filteredCycles = sortBy(filteredCycles, [ + (c) => STATUS_ORDER[c.status.toLowerCase()], + (c) => (c.status.toLowerCase() === "upcoming" ? c.start_date : c.name.toLowerCase()), + ]); + + return filteredCycles; +}; + +/** + * @description filters cycles based on the filter + * @param {ICycle} cycle + * @param {TCycleFilters} filter + * @returns {boolean} + */ +export const shouldFilterCycle = (cycle: ICycle, filter: TCycleFilters): boolean => { + let fallsInFilters = true; + Object.keys(filter).forEach((key) => { + const filterKey = key as keyof TCycleFilters; + if (filterKey === "status" && filter.status && filter.status.length > 0) + fallsInFilters = fallsInFilters && filter.status.includes(cycle.status.toLowerCase()); + if (filterKey === "start_date" && filter.start_date && filter.start_date.length > 0) { + filter.start_date.forEach((dateFilter) => { + fallsInFilters = + fallsInFilters && !!cycle.start_date && satisfiesDateFilter(new Date(cycle.start_date), dateFilter); + }); + } + if (filterKey === "end_date" && filter.end_date && filter.end_date.length > 0) { + filter.end_date.forEach((dateFilter) => { + fallsInFilters = + fallsInFilters && !!cycle.end_date && satisfiesDateFilter(new Date(cycle.end_date), dateFilter); + }); + } + }); + + return fallsInFilters; +}; diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts index d31a25b3d..3c34fa9da 100644 --- a/web/helpers/filter.helper.ts +++ b/web/helpers/filter.helper.ts @@ -1,3 +1,4 @@ +import { differenceInCalendarDays } from "date-fns"; // types import { IIssueFilterOptions } from "@plane/types"; @@ -13,3 +14,29 @@ export const calculateTotalFilters = (filters: IIssueFilterOptions): number => ) .reduce((curr, prev) => curr + prev, 0) : 0; + +/** + * @description checks if the date satisfies the filter + * @param {Date} date + * @param {string} filter + * @returns {boolean} + */ +export const satisfiesDateFilter = (date: Date, filter: string): boolean => { + const [value, operator, from] = filter.split(";"); + + if (!from) { + if (operator === "after") return date >= new Date(value); + if (operator === "before") return date <= new Date(value); + } + + if (from === "fromnow") { + if (operator === "after") { + if (value === "1_weeks") return differenceInCalendarDays(date, new Date()) >= 7; + if (value === "2_weeks") return differenceInCalendarDays(date, new Date()) >= 14; + if (value === "1_months") return differenceInCalendarDays(date, new Date()) >= 30; + if (value === "2_months") return differenceInCalendarDays(date, new Date()) >= 60; + } + } + + return false; +}; diff --git a/web/hooks/store/index.ts b/web/hooks/store/index.ts index ff036a529..3ec5c97bf 100644 --- a/web/hooks/store/index.ts +++ b/web/hooks/store/index.ts @@ -1,7 +1,8 @@ export * from "./use-application"; -export * from "./use-event-tracker"; export * from "./use-calendar-view"; +export * from "./use-cycle-filter"; export * from "./use-cycle"; +export * from "./use-event-tracker"; export * from "./use-dashboard"; export * from "./use-estimate"; export * from "./use-global-view"; diff --git a/web/hooks/store/use-cycle-filter.ts b/web/hooks/store/use-cycle-filter.ts new file mode 100644 index 000000000..50c37508b --- /dev/null +++ b/web/hooks/store/use-cycle-filter.ts @@ -0,0 +1,11 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "contexts/store-context"; +// types +import { ICycleFilterStore } from "store/cycle_filter.store"; + +export const useCycleFilter = (): ICycleFilterStore => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useCycleFilter must be used within StoreProvider"); + return context.cycleFilter; +}; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx index a22e252f2..fa9008d2f 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/index.tsx @@ -1,28 +1,35 @@ -import { Fragment, useCallback, useState, ReactElement } from "react"; +import { Fragment, useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Tab } from "@headlessui/react"; // hooks -import { useEventTracker, useCycle, useProject } from "hooks/store"; -import useLocalStorage from "hooks/use-local-storage"; +import { useEventTracker, useCycle, useProject, useCycleFilter } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components import { PageHead } from "components/core"; import { CyclesHeader } from "components/headers"; -import { CyclesView, ActiveCycleDetails, CycleCreateUpdateModal } from "components/cycles"; +import { + CyclesView, + CycleCreateUpdateModal, + CyclesViewHeader, + CycleAppliedFiltersList, + ActiveCycleRoot, +} from "components/cycles"; import { EmptyState } from "components/empty-state"; -import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; // ui -import { Tooltip } from "@plane/ui"; +import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "components/ui"; +// helpers +import { calculateTotalFilters } from "helpers/filter.helper"; // types import { NextPageWithLayout } from "lib/types"; -import { TCycleView, TCycleLayout } from "@plane/types"; +import { TCycleFilters } from "@plane/types"; // constants -import { CYCLE_TAB_LIST, CYCLE_VIEW_LAYOUTS } from "constants/cycle"; +import { CYCLE_TABS_LIST } from "constants/cycle"; import { EmptyStateType } from "constants/empty-state"; const ProjectCyclesPage: NextPageWithLayout = observer(() => { + // states const [createModal, setCreateModal] = useState(false); // store hooks const { setTrackElement } = useEventTracker(); @@ -31,28 +38,26 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { // router const router = useRouter(); const { workspaceSlug, projectId, peekCycle } = router.query; - // local storage - const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycle_tab", "active"); - const { storedValue: cycleLayout, setValue: setCycleLayout } = useLocalStorage("cycle_layout", "list"); + // cycle filters hook + const { clearAllFilters, currentProjectDisplayFilters, currentProjectFilters, updateDisplayFilters, updateFilters } = + useCycleFilter(); // derived values const totalCycles = currentProjectCycleIds?.length ?? 0; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; + // selected display filters + const cycleTab = currentProjectDisplayFilters?.active_tab; + const cycleLayout = currentProjectDisplayFilters?.layout; - const handleCurrentLayout = useCallback( - (_layout: TCycleLayout) => { - setCycleLayout(_layout); - }, - [setCycleLayout] - ); + const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { + if (!projectId) return; + let newValues = currentProjectFilters?.[key] ?? []; - const handleCurrentView = useCallback( - (_view: TCycleView) => { - setCycleTab(_view); - if (_view === "draft") handleCurrentLayout("list"); - }, - [handleCurrentLayout, setCycleTab] - ); + if (!value) newValues = []; + else newValues = newValues.filter((val) => val !== value); + + updateFilters(projectId.toString(), { [key]: newValues }); + }; if (!workspaceSlug || !projectId) return null; @@ -89,101 +94,35 @@ const ProjectCyclesPage: NextPageWithLayout = observer(() => { i.key == cycleTab)} - selectedIndex={CYCLE_TAB_LIST.findIndex((i) => i.key == cycleTab)} - onChange={(i) => handleCurrentView(CYCLE_TAB_LIST[i]?.key ?? "active")} + defaultIndex={CYCLE_TABS_LIST.findIndex((i) => i.key == cycleTab)} + selectedIndex={CYCLE_TABS_LIST.findIndex((i) => i.key == cycleTab)} + onChange={(i) => { + if (!projectId) return; + const tab = CYCLE_TABS_LIST[i]; + if (!tab) return; + updateDisplayFilters(projectId.toString(), { + active_tab: tab.key, + }); + }} > -
- - {CYCLE_TAB_LIST.map((tab) => ( - - `border-b-2 p-4 text-sm font-medium outline-none ${ - selected ? "border-custom-primary-100 text-custom-primary-100" : "border-transparent" - }` - } - > - {tab.name} - - ))} - -
- {cycleTab !== "active" && ( -
- {CYCLE_VIEW_LAYOUTS.map((layout) => { - if (layout.key === "gantt" && cycleTab === "draft") return null; - - return ( - - - - ); - })} -
- )} + + {calculateTotalFilters(currentProjectFilters ?? {}) !== 0 && ( +
+ clearAllFilters(projectId.toString())} + handleRemoveFilter={handleRemoveFilter} + />
-
- + )} - - {cycleTab && cycleLayout && ( - - )} - - - + - {cycleTab && cycleLayout && ( - )} - - - - {cycleTab && cycleLayout && workspaceSlug && projectId && ( - - )} - - - - {cycleTab && cycleLayout && workspaceSlug && projectId && ( - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/public/empty-state/cycle/name-filter.svg b/web/public/empty-state/cycle/name-filter.svg new file mode 100644 index 000000000..168611119 --- /dev/null +++ b/web/public/empty-state/cycle/name-filter.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts index aea87033e..71cb8f924 100644 --- a/web/store/cycle.store.ts +++ b/web/store/cycle.store.ts @@ -11,9 +11,10 @@ import { IssueService } from "services/issue"; import { ProjectService } from "services/project"; import { RootStore } from "store/root.store"; import { ICycle, CycleDateCheckData } from "@plane/types"; +import { orderCycles, shouldFilterCycle } from "helpers/cycle.helper"; export interface ICycleStore { - //Loaders + // loaders loader: boolean; // observables fetchedMap: Record; @@ -27,6 +28,8 @@ export interface ICycleStore { currentProjectDraftCycleIds: string[] | null; currentProjectActiveCycleId: string | null; // computed actions + getFilteredCycleIds: (projectId: string) => string[] | null; + getFilteredCompletedCycleIds: (projectId: string) => string[] | null; getCycleById: (cycleId: string) => ICycle | null; getCycleNameById: (cycleId: string) => string | undefined; getActiveCycleById: (cycleId: string) => ICycle | null; @@ -183,6 +186,49 @@ export class CycleStore implements ICycleStore { return activeCycle || null; } + /** + * @description returns filtered cycle ids based on display filters and filters + * @param {TCycleDisplayFilters} displayFilters + * @param {TCycleFilters} filters + * @returns {string[] | null} + */ + getFilteredCycleIds = computedFn((projectId: string) => { + const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId); + const searchQuery = this.rootStore.cycleFilter.searchQuery; + if (!this.fetchedMap[projectId]) return null; + let cycles = Object.values(this.cycleMap ?? {}).filter( + (c) => + c.project_id === projectId && + c.name.toLowerCase().includes(searchQuery.toLowerCase()) && + shouldFilterCycle(c, filters ?? {}) + ); + cycles = orderCycles(cycles); + const cycleIds = cycles.map((c) => c.id); + return cycleIds; + }); + + /** + * @description returns filtered cycle ids based on display filters and filters + * @param {TCycleDisplayFilters} displayFilters + * @param {TCycleFilters} filters + * @returns {string[] | null} + */ + getFilteredCompletedCycleIds = computedFn((projectId: string) => { + const filters = this.rootStore.cycleFilter.getFiltersByProjectId(projectId); + const searchQuery = this.rootStore.cycleFilter.searchQuery; + if (!this.fetchedMap[projectId]) return null; + let cycles = Object.values(this.cycleMap ?? {}).filter( + (c) => + c.project_id === projectId && + c.status.toLowerCase() === "completed" && + c.name.toLowerCase().includes(searchQuery.toLowerCase()) && + shouldFilterCycle(c, filters ?? {}) + ); + cycles = sortBy(cycles, [(c) => !c.start_date]); + const cycleIds = cycles.map((c) => c.id); + return cycleIds; + }); + /** * @description returns cycle details by cycle id * @param cycleId diff --git a/web/store/cycle_filter.store.ts b/web/store/cycle_filter.store.ts new file mode 100644 index 000000000..064ea4a4e --- /dev/null +++ b/web/store/cycle_filter.store.ts @@ -0,0 +1,145 @@ +import { action, computed, observable, makeObservable, runInAction, autorun } from "mobx"; +import { computedFn } from "mobx-utils"; +import set from "lodash/set"; +// types +import { RootStore } from "store/root.store"; +import { TCycleDisplayFilters, TCycleFilters } from "@plane/types"; + +export interface ICycleFilterStore { + // observables + displayFilters: Record; + filters: Record; + searchQuery: string; + // computed + currentProjectDisplayFilters: TCycleDisplayFilters | undefined; + currentProjectFilters: TCycleFilters | undefined; + // computed functions + getDisplayFiltersByProjectId: (projectId: string) => TCycleDisplayFilters | undefined; + getFiltersByProjectId: (projectId: string) => TCycleFilters | undefined; + // actions + updateDisplayFilters: (projectId: string, displayFilters: TCycleDisplayFilters) => void; + updateFilters: (projectId: string, filters: TCycleFilters) => void; + updateSearchQuery: (query: string) => void; + clearAllFilters: (projectId: string) => void; +} + +export class CycleFilterStore implements ICycleFilterStore { + // observables + displayFilters: Record = {}; + filters: Record = {}; + searchQuery: string = ""; + // root store + rootStore: RootStore; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + displayFilters: observable, + filters: observable, + searchQuery: observable.ref, + // computed + currentProjectDisplayFilters: computed, + currentProjectFilters: computed, + // actions + updateDisplayFilters: action, + updateFilters: action, + updateSearchQuery: action, + clearAllFilters: action, + }); + // root store + this.rootStore = _rootStore; + // initialize display filters of the current project + autorun(() => { + const projectId = this.rootStore.app.router.projectId; + if (!projectId) return; + this.initProjectCycleFilters(projectId); + }); + } + + /** + * @description get display filters of the current project + */ + get currentProjectDisplayFilters() { + const projectId = this.rootStore.app.router.projectId; + if (!projectId) return; + return this.displayFilters[projectId]; + } + + /** + * @description get filters of the current project + */ + get currentProjectFilters() { + const projectId = this.rootStore.app.router.projectId; + if (!projectId) return; + return this.filters[projectId]; + } + + /** + * @description get display filters of a project by projectId + * @param {string} projectId + */ + getDisplayFiltersByProjectId = computedFn((projectId: string) => this.displayFilters[projectId]); + + /** + * @description get filters of a project by projectId + * @param {string} projectId + */ + getFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId]); + + /** + * @description initialize display filters and filters of a project + * @param {string} projectId + */ + initProjectCycleFilters = (projectId: string) => { + const displayFilters = this.getDisplayFiltersByProjectId(projectId); + runInAction(() => { + this.displayFilters[projectId] = { + active_tab: displayFilters?.active_tab || "active", + layout: displayFilters?.layout || "list", + }; + this.filters[projectId] = {}; + }); + }; + + /** + * @description update display filters of a project + * @param {string} projectId + * @param {TCycleDisplayFilters} displayFilters + */ + updateDisplayFilters = (projectId: string, displayFilters: TCycleDisplayFilters) => { + runInAction(() => { + Object.keys(displayFilters).forEach((key) => { + set(this.displayFilters, [projectId, key], displayFilters[key as keyof TCycleDisplayFilters]); + }); + }); + }; + + /** + * @description update filters of a project + * @param {string} projectId + * @param {TCycleFilters} filters + */ + updateFilters = (projectId: string, filters: TCycleFilters) => { + runInAction(() => { + Object.keys(filters).forEach((key) => { + set(this.filters, [projectId, key], filters[key as keyof TCycleFilters]); + }); + }); + }; + + /** + * @description update search query + * @param {string} query + */ + updateSearchQuery = (query: string) => (this.searchQuery = query); + + /** + * @description clear all filters of a project + * @param {string} projectId + */ + clearAllFilters = (projectId: string) => { + runInAction(() => { + this.filters[projectId] = {}; + }); + }; +} diff --git a/web/store/root.store.ts b/web/store/root.store.ts index 298cd532e..0390d7ce2 100644 --- a/web/store/root.store.ts +++ b/web/store/root.store.ts @@ -18,6 +18,7 @@ import { IStateStore, StateStore } from "./state.store"; import { IUserRootStore, UserRootStore } from "./user"; import { IWorkspaceRootStore, WorkspaceRootStore } from "./workspace"; import { IProjectPageStore, ProjectPageStore } from "./project-page.store"; +import { CycleFilterStore, ICycleFilterStore } from "./cycle_filter.store"; enableStaticRendering(typeof window === "undefined"); @@ -29,6 +30,7 @@ export class RootStore { projectRoot: IProjectRootStore; memberRoot: IMemberRootStore; cycle: ICycleStore; + cycleFilter: ICycleFilterStore; module: IModuleStore; projectView: IProjectViewStore; globalView: IGlobalViewStore; @@ -50,6 +52,7 @@ export class RootStore { this.memberRoot = new MemberRootStore(this); // independent stores this.cycle = new CycleStore(this); + this.cycleFilter = new CycleFilterStore(this); this.module = new ModulesStore(this); this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); @@ -69,6 +72,7 @@ export class RootStore { this.memberRoot = new MemberRootStore(this); // independent stores this.cycle = new CycleStore(this); + this.cycleFilter = new CycleFilterStore(this); this.module = new ModulesStore(this); this.projectView = new ProjectViewStore(this); this.globalView = new GlobalViewStore(this); From 89d019df5706d76b25d5bbd798f1ea00d6e9fd5d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:01:07 +0530 Subject: [PATCH 09/24] fix: parent select modal params updated (#3914) --- web/components/issues/issue-modal/form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/issues/issue-modal/form.tsx b/web/components/issues/issue-modal/form.tsx index 5f2ef0775..f585fae55 100644 --- a/web/components/issues/issue-modal/form.tsx +++ b/web/components/issues/issue-modal/form.tsx @@ -703,6 +703,7 @@ export const IssueFormRoot: FC = observer((props) => { setSelectedParentIssue(issue); }} projectId={projectId} + issueId={data?.id} /> )} /> From c8ab6500086872ec23b11dcf377eafe89128681e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:01:27 +0530 Subject: [PATCH 10/24] fix: module empty state create issue (#3916) --- web/components/issues/issue-layouts/empty-states/module.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/issues/issue-layouts/empty-states/module.tsx b/web/components/issues/issue-layouts/empty-states/module.tsx index 6c0cd0cd6..d8e3516cd 100644 --- a/web/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/components/issues/issue-layouts/empty-states/module.tsx @@ -77,8 +77,8 @@ export const ModuleEmptyState: React.FC = observer((props) => { isEmptyFilters ? undefined : () => { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + setTrackElement("Module issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.MODULE); } } secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} From 6c7ba3bc79130018e5167fe0343150d7842007e8 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:01:59 +0530 Subject: [PATCH 11/24] chore: applied filter ui improvement (#3918) --- .../issue-layouts/filters/applied-filters/filters-list.tsx | 4 ++-- .../filters/applied-filters/roots/archived-issue.tsx | 2 +- .../filters/applied-filters/roots/cycle-root.tsx | 2 +- .../filters/applied-filters/roots/draft-issue.tsx | 2 +- .../filters/applied-filters/roots/module-root.tsx | 2 +- .../filters/applied-filters/roots/project-root.tsx | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx index 467258273..94f2b30d1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/filters-list.tsx @@ -62,8 +62,8 @@ export const AppliedFiltersList: React.FC = observer((props) => { key={filterKey} className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize" > - {replaceUnderscoreIfSnakeCase(filterKey)} -
+
+ {replaceUnderscoreIfSnakeCase(filterKey)} {membersFilters.includes(filterKey) && ( { if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
{ if (Object.keys(appliedFilters).length === 0 || !workspaceSlug || !projectId) return null; return ( -
+
{ if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
{ if (!workspaceSlug || !projectId || Object.keys(appliedFilters).length === 0) return null; return ( -
+
{ if (Object.keys(appliedFilters).length === 0) return null; return ( -
+
Date: Mon, 11 Mar 2024 21:02:28 +0530 Subject: [PATCH 12/24] chore: issue quick action dropdown max height updated (#3919) --- .../issues/issue-layouts/quick-action-dropdowns/all-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/archived-issue.tsx | 1 + .../issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx | 1 + .../issues/issue-layouts/quick-action-dropdowns/module-issue.tsx | 1 + .../issue-layouts/quick-action-dropdowns/project-issue.tsx | 1 + 5 files changed, 5 insertions(+) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index f6c63191f..e1ff30f9b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -99,6 +99,7 @@ export const AllIssueQuickActions: React.FC = observer((props placement="bottom-start" customButton={customActionButton} portalElement={portalElement} + maxHeight="lg" closeOnSelect ellipsis > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index dae88a387..9cf394a1b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -60,6 +60,7 @@ export const ArchivedIssueQuickActions: React.FC = (props) => placement="bottom-start" customButton={customActionButton} portalElement={portalElement} + maxHeight="lg" closeOnSelect ellipsis > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index fe713ed23..38b38926f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -110,6 +110,7 @@ export const CycleIssueQuickActions: React.FC = observer((pro placement="bottom-start" customButton={customActionButton} portalElement={portalElement} + maxHeight="lg" closeOnSelect ellipsis > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index f24f6869e..00d69cba3 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -109,6 +109,7 @@ export const ModuleIssueQuickActions: React.FC = observer((pr placement="bottom-start" customButton={customActionButton} portalElement={portalElement} + maxHeight="lg" closeOnSelect ellipsis > diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 24a2433d5..a198b6104 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -110,6 +110,7 @@ export const ProjectIssueQuickActions: React.FC = observer((p placement="bottom-start" customButton={customActionButton} portalElement={portalElement} + maxHeight="lg" closeOnSelect ellipsis > From 27acd1bec17d04cbe232b5156d29231402159daa Mon Sep 17 00:00:00 2001 From: Lakhan Baheti <94619783+1akhanBaheti@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:03:30 +0530 Subject: [PATCH 13/24] fix: cycle & module sidebar progress-stats scroll (#3932) --- web/components/core/sidebar/sidebar-progress-stats.tsx | 6 +++--- web/components/cycles/sidebar.tsx | 10 +++++----- web/components/modules/sidebar.tsx | 6 +++--- .../projects/[projectId]/cycles/[cycleId].tsx | 2 +- .../projects/[projectId]/modules/[moduleId].tsx | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/web/components/core/sidebar/sidebar-progress-stats.tsx b/web/components/core/sidebar/sidebar-progress-stats.tsx index 6ff3d3f1e..5fd576081 100644 --- a/web/components/core/sidebar/sidebar-progress-stats.tsx +++ b/web/components/core/sidebar/sidebar-progress-stats.tsx @@ -127,7 +127,7 @@ export const SidebarProgressStats: React.FC = ({ {distribution?.assignees.length > 0 ? ( distribution.assignees.map((assignee, index) => { @@ -187,7 +187,7 @@ export const SidebarProgressStats: React.FC = ({ {distribution?.labels.length > 0 ? ( distribution.labels.map((label, index) => ( @@ -230,7 +230,7 @@ export const SidebarProgressStats: React.FC = ({ {Object.keys(groupedIssues).map((group, index) => ( = observer((props) => { ? "0 Issue" : `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}` : cycleDetails.total_issues === 0 - ? "0 Issue" - : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; + ? "0 Issue" + : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; return ( - <> +
{cycleDetails && workspaceSlug && projectId && ( = observer((props) => { )} <> -
+
- +
); }); diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx index c9f28cf98..55be14a60 100644 --- a/web/components/modules/sidebar.tsx +++ b/web/components/modules/sidebar.tsx @@ -242,7 +242,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; return ( - <> +
{ @@ -257,7 +257,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { setModuleDeleteModal(false)} data={moduleDetails} /> <> -
+
- +
); }); diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 6eaef6c0f..6cf76cd70 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -67,7 +67,7 @@ const CycleDetailPage: NextPageWithLayout = observer(() => {
{cycleId && !isSidebarCollapsed && (
{
{moduleId && !isSidebarCollapsed && (
Date: Mon, 11 Mar 2024 21:04:43 +0530 Subject: [PATCH 14/24] fix: caching (#3923) --- apiserver/bin/takeoff | 8 +++++-- apiserver/bin/takeoff.local | 5 +++- .../db/management/commands/clear_cache.py | 17 +++++++++++++ apiserver/plane/utils/cache.py | 24 +++++++++++-------- 4 files changed, 41 insertions(+), 13 deletions(-) create mode 100644 apiserver/plane/db/management/commands/clear_cache.py diff --git a/apiserver/bin/takeoff b/apiserver/bin/takeoff index efea53f87..5a1da1570 100755 --- a/apiserver/bin/takeoff +++ b/apiserver/bin/takeoff @@ -21,11 +21,15 @@ SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256 export MACHINE_SIGNATURE=$SIGNATURE # Register instance -python manage.py register_instance $MACHINE_SIGNATURE +python manage.py register_instance "$MACHINE_SIGNATURE" + # Load the configuration variable python manage.py configure_instance # Create the default bucket python manage.py create_bucket -exec gunicorn -w $GUNICORN_WORKERS -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:${PORT:-8000} --max-requests 1200 --max-requests-jitter 1000 --access-logfile - +# Clear Cache before starting to remove stale values +python manage.py clear_cache + +exec gunicorn -w "$GUNICORN_WORKERS" -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:"${PORT:-8000}" --max-requests 1200 --max-requests-jitter 1000 --access-logfile - diff --git a/apiserver/bin/takeoff.local b/apiserver/bin/takeoff.local index 8f62370ec..3194009b2 100755 --- a/apiserver/bin/takeoff.local +++ b/apiserver/bin/takeoff.local @@ -21,12 +21,15 @@ SIGNATURE=$(echo "$HOSTNAME$MAC_ADDRESS$CPU_INFO$MEMORY_INFO$DISK_INFO" | sha256 export MACHINE_SIGNATURE=$SIGNATURE # Register instance -python manage.py register_instance $MACHINE_SIGNATURE +python manage.py register_instance "$MACHINE_SIGNATURE" # Load the configuration variable python manage.py configure_instance # Create the default bucket python manage.py create_bucket +# Clear Cache before starting to remove stale values +python manage.py clear_cache + python manage.py runserver 0.0.0.0:8000 --settings=plane.settings.local diff --git a/apiserver/plane/db/management/commands/clear_cache.py b/apiserver/plane/db/management/commands/clear_cache.py new file mode 100644 index 000000000..4dfbe6c10 --- /dev/null +++ b/apiserver/plane/db/management/commands/clear_cache.py @@ -0,0 +1,17 @@ +# Django imports +from django.core.cache import cache +from django.core.management import BaseCommand + + +class Command(BaseCommand): + help = "Clear Cache before starting the server to remove stale values" + + def handle(self, *args, **options): + try: + cache.clear() + self.stdout.write(self.style.SUCCESS("Cache Cleared")) + return + except Exception: + # Another ClientError occurred + self.stdout.write(self.style.ERROR("Failed to clear cache")) + return diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py index dba89c4a6..aece1d644 100644 --- a/apiserver/plane/utils/cache.py +++ b/apiserver/plane/utils/cache.py @@ -1,7 +1,11 @@ -from django.core.cache import cache -# from django.utils.encoding import force_bytes -# import hashlib +# Python imports from functools import wraps + +# Django imports +from django.conf import settings +from django.core.cache import cache + +# Third party imports from rest_framework.response import Response @@ -22,21 +26,20 @@ def cache_response(timeout=60 * 60, path=None, user=True): def _wrapped_view(instance, request, *args, **kwargs): # Function to generate cache key auth_header = ( - None if request.user.is_anonymous else str(request.user.id) if user else None + None + if request.user.is_anonymous + else str(request.user.id) if user else None ) custom_path = path if path is not None else request.get_full_path() key = generate_cache_key(custom_path, auth_header) cached_result = cache.get(key) if cached_result is not None: - print("Cache Hit") return Response( cached_result["data"], status=cached_result["status"] ) - - print("Cache Miss") response = view_func(instance, request, *args, **kwargs) - if response.status_code == 200: + if response.status_code == 200 and not settings.DEBUG: cache.set( key, {"data": response.data, "status": response.status_code}, @@ -71,11 +74,12 @@ def invalidate_cache(path=None, url_params=False, user=True): ) auth_header = ( - None if request.user.is_anonymous else str(request.user.id) if user else None + None + if request.user.is_anonymous + else str(request.user.id) if user else None ) key = generate_cache_key(custom_path, auth_header) cache.delete(key) - print("Invalidating cache") # Execute the view function return view_func(instance, request, *args, **kwargs) From 01702e9f66f5a6700b786798bcc9591b180b957d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:05:00 +0530 Subject: [PATCH 15/24] [WEB-402] chore: project issues count (#3911) * chore: added project issues count * chore: project module and cycle issue count * chore: resolved merge conflicts * chore: added import statement * chore: issue count type added * chore: issue count added in project, cycle and module issues * fix: lint fixes * chore: tooltip added in issue count badge * chore: tooltip added in issue count badge --------- Co-authored-by: NarayanBavisetti Co-authored-by: sriram veeraghanta --- apiserver/plane/app/serializers/module.py | 3 +- apiserver/plane/app/serializers/project.py | 6 ++ apiserver/plane/app/views/cycle/base.py | 62 ++++++++++------ apiserver/plane/app/views/module/base.py | 39 +++++++---- apiserver/plane/app/views/project/base.py | 70 +++++++++++++++++++ packages/types/src/cycle/cycle.d.ts | 1 + packages/types/src/modules.d.ts | 1 + packages/types/src/projects.d.ts | 6 ++ web/components/headers/cycle-issues.tsx | 30 ++++++-- web/components/headers/module-issues.tsx | 28 ++++++-- .../headers/project-archived-issues.tsx | 20 +++++- .../headers/project-draft-issues.tsx | 21 +++++- web/components/headers/project-issues.tsx | 20 +++++- 13 files changed, 256 insertions(+), 51 deletions(-) diff --git a/apiserver/plane/app/serializers/module.py b/apiserver/plane/app/serializers/module.py index 3b2468aee..100b6314a 100644 --- a/apiserver/plane/app/serializers/module.py +++ b/apiserver/plane/app/serializers/module.py @@ -215,9 +215,10 @@ class ModuleSerializer(DynamicBaseSerializer): class ModuleDetailSerializer(ModuleSerializer): link_module = ModuleLinkSerializer(read_only=True, many=True) + sub_issues = serializers.IntegerField(read_only=True) class Meta(ModuleSerializer.Meta): - fields = ModuleSerializer.Meta.fields + ["link_module"] + fields = ModuleSerializer.Meta.fields + ["link_module", "sub_issues"] class ModuleFavoriteSerializer(BaseSerializer): diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index 6840fa8f7..a0c2318e3 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -102,6 +102,12 @@ class ProjectLiteSerializer(BaseSerializer): class ProjectListSerializer(DynamicBaseSerializer): + total_issues = serializers.IntegerField(read_only=True) + archived_issues = serializers.IntegerField(read_only=True) + archived_sub_issues = serializers.IntegerField(read_only=True) + draft_issues = serializers.IntegerField(read_only=True) + draft_sub_issues = serializers.IntegerField(read_only=True) + sub_issues = serializers.IntegerField(read_only=True) is_favorite = serializers.BooleanField(read_only=True) total_members = serializers.IntegerField(read_only=True) total_cycles = serializers.IntegerField(read_only=True) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 9dc25474f..42904a8fc 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -106,15 +106,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): ) ) .annotate(is_favorite=Exists(favorite_subquery)) - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) .annotate( completed_issues=Count( "issue_cycle__issue__state__group", @@ -232,7 +223,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -327,13 +317,13 @@ class CycleViewSet(WebhookMixin, BaseViewSet): } if data[0]["start_date"] and data[0]["end_date"]: - data[0]["distribution"][ - "completion_chart" - ] = burndown_plot( - queryset=queryset.first(), - slug=slug, - project_id=project_id, - cycle_id=data[0]["id"], + data[0]["distribution"]["completion_chart"] = ( + burndown_plot( + queryset=queryset.first(), + slug=slug, + project_id=project_id, + cycle_id=data[0]["id"], + ) ) return Response(data, status=status.HTTP_200_OK) @@ -356,7 +346,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -402,7 +391,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -474,7 +462,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "progress_snapshot", # meta fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -487,10 +474,42 @@ class CycleViewSet(WebhookMixin, BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk): - queryset = self.get_queryset().filter(pk=pk) + queryset = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Count( + "issue_cycle", + filter=Q( + issue_cycle__issue__archived_at__isnull=True, + issue_cycle__issue__is_draft=False, + ), + ) + ) + ) data = ( self.get_queryset() .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=True, + issue_cycle__cycle_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=False, + issue_cycle__cycle_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) .values( # necessary fields "id", @@ -507,6 +526,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet): "external_source", "external_id", "progress_snapshot", + "sub_issues", # meta fields "is_favorite", "total_issues", diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index cd87442d2..ee9718b59 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -3,7 +3,7 @@ import json # Django Imports from django.utils import timezone -from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q +from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q, Func from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import Value, UUIDField @@ -79,15 +79,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ), ) ) - .annotate( - total_issues=Count( - "issue_module", - filter=Q( - issue_module__issue__archived_at__isnull=True, - issue_module__issue__is_draft=False, - ), - ), - ) .annotate( completed_issues=Count( "issue_module__issue__state__group", @@ -183,7 +174,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -225,7 +215,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", @@ -237,7 +226,30 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): return Response(modules, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk): - queryset = self.get_queryset().filter(pk=pk) + queryset = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=True, + issue_module__module_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("project_id"), + parent__isnull=False, + issue_module__module_id=pk, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ) assignee_distribution = ( Issue.objects.filter( @@ -380,7 +392,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): "external_id", # computed fields "is_favorite", - "total_issues", "cancelled_issues", "completed_issues", "started_issues", diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 6deeea144..74d4e3466 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -46,9 +46,11 @@ from plane.db.models import ( Inbox, ProjectDeployBoard, IssueProperty, + Issue, ) from plane.utils.cache import cache_response + class ProjectViewSet(WebhookMixin, BaseViewSet): serializer_class = ProjectListSerializer model = Project @@ -171,6 +173,73 @@ class ProjectViewSet(WebhookMixin, BaseViewSet): ).data return Response(projects, status=status.HTTP_200_OK) + def retrieve(self, request, slug, pk): + project = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + total_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("pk"), + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues=Issue.issue_objects.filter( + project_id=self.kwargs.get("pk"), + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + archived_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + archived_at__isnull=False, + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + archived_sub_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + archived_at__isnull=False, + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + draft_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + is_draft=True, + parent__isnull=True, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + draft_sub_issues=Issue.objects.filter( + project_id=self.kwargs.get("pk"), + is_draft=True, + parent__isnull=False, + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ).first() + + serializer = ProjectListSerializer(project) + return Response(serializer.data, status=status.HTTP_200_OK) + def create(self, request, slug): try: workspace = Workspace.objects.get(slug=slug) @@ -471,6 +540,7 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] + # Cache the below api for 24 hours @cache_response(60 * 60 * 24, user=False) def get(self, request): diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index 6d21e05b8..c41ab279b 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -26,6 +26,7 @@ export interface ICycle { sort_order: number; start_date: string | null; started_issues: number; + sub_issues: number; total_issues: number; unstarted_issues: number; updated_at: Date; diff --git a/packages/types/src/modules.d.ts b/packages/types/src/modules.d.ts index c532a467c..0af293e50 100644 --- a/packages/types/src/modules.d.ts +++ b/packages/types/src/modules.d.ts @@ -30,6 +30,7 @@ export interface IModule { name: string; project_id: string; sort_order: number; + sub_issues: number; start_date: string | null; started_issues: number; status: TModuleStatus; diff --git a/packages/types/src/projects.d.ts b/packages/types/src/projects.d.ts index a6da364b9..afae5199f 100644 --- a/packages/types/src/projects.d.ts +++ b/packages/types/src/projects.d.ts @@ -23,6 +23,8 @@ export type TProjectLogoProps = { export interface IProject { archive_in: number; + archived_issues: number; + archived_sub_issues: number; close_in: number; created_at: Date; created_by: string; @@ -35,6 +37,8 @@ export interface IProject { default_assignee: IUser | string | null; default_state: string | null; description: string; + draft_issues: number; + draft_sub_issues: number; estimate: string | null; id: string; identifier: string; @@ -48,7 +52,9 @@ export interface IProject { network: number; project_lead: IUserLite | string | null; sort_order: number | null; + sub_issues: number; total_cycles: number; + total_issues: number; total_members: number; total_modules: number; updated_at: Date; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 5ef1ebf2c..6f9c61545 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; // hooks import { ArrowRight, Plus, PanelRight } from "lucide-react"; -import { Breadcrumbs, Button, ContrastIcon, CustomMenu } from "@plane/ui"; +import { Breadcrumbs, Button, ContrastIcon, CustomMenu, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -142,6 +142,12 @@ export const CycleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = cycleDetails + ? issueFilters?.displayFilters?.sub_issue + ? cycleDetails.total_issues + cycleDetails?.sub_issues + : cycleDetails.total_issues + : undefined; + return ( <> { label={ <> -
- {cycleDetails?.name && cycleDetails.name} +
+

{cycleDetails?.name && cycleDetails.name}

+ {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue" + } in this cycle`} + position="bottom" + > + + {issueCount} + + + ) : null}
} - className="ml-1.5 flex-shrink-0" + className="ml-1.5 flex-shrink-0 truncate" placement="bottom-start" > - {currentProjectCycleIds?.map((cycleId) => )} + {currentProjectCycleIds?.map((cycleId) => ( + + ))} } /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 10717ecc3..9505d7145 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; // hooks import { ArrowRight, PanelRight, Plus } from "lucide-react"; -import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -143,6 +143,12 @@ export const ModuleIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = moduleDetails + ? issueFilters?.displayFilters?.sub_issue + ? moduleDetails.total_issues + moduleDetails.sub_issues + : moduleDetails.total_issues + : undefined; + return ( <> { label={ <> -
- {moduleDetails?.name && moduleDetails.name} +
+

{moduleDetails?.name && moduleDetails.name}

+ {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue" + } in this module`} + position="bottom" + > + + {issueCount} + + + ) : null}
} className="ml-1.5 flex-shrink-0" placement="bottom-start" > - {projectModuleIds?.map((moduleId) => )} + {projectModuleIds?.map((moduleId) => ( + + ))} } /> diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index db208aa21..ce226b58e 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -5,7 +5,7 @@ import { ArrowLeft } from "lucide-react"; // hooks // constants // ui -import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; // components import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -69,6 +69,12 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.DISPLAY_PROPERTIES, property); }; + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails.archived_issues + currentProjectDetails.archived_sub_issues + : currentProjectDetails.archived_issues + : undefined; + return (
@@ -82,7 +88,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
-
+
{ } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in project's archived`} + position="bottom" + > + + {issueCount} + + + ) : null}
diff --git a/web/components/headers/project-draft-issues.tsx b/web/components/headers/project-draft-issues.tsx index 4f2929621..789c3f60f 100644 --- a/web/components/headers/project-draft-issues.tsx +++ b/web/components/headers/project-draft-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; // hooks // components -import { Breadcrumbs, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues"; @@ -73,11 +73,18 @@ export const ProjectDraftIssueHeader: FC = observer(() => { }, [workspaceSlug, projectId, updateFilters] ); + + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails.draft_issues + currentProjectDetails.draft_sub_issues + : currentProjectDetails.draft_issues + : undefined; + return (
-
+
{ } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in project's draft`} + position="bottom" + > + + {issueCount} + + + ) : null}
diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 19eaf4f4f..9739e7832 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; // hooks -import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; +import { Breadcrumbs, Button, LayersIcon, Tooltip } from "@plane/ui"; import { ProjectAnalyticsModal } from "components/analytics"; import { BreadcrumbLink } from "components/common"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; @@ -102,6 +102,12 @@ export const ProjectIssuesHeader: React.FC = observer(() => { const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); + const issueCount = currentProjectDetails + ? issueFilters?.displayFilters?.sub_issue + ? currentProjectDetails?.total_issues + currentProjectDetails?.sub_issues + : currentProjectDetails?.total_issues + : undefined; + return ( <> {
-
+
router.back()}> { } /> + {issueCount && issueCount > 0 ? ( + 1 ? "issues" : "issue"} in this project`} + position="bottom" + > + + {issueCount} + + + ) : null}
{currentProjectDetails?.is_deployed && deployUrl && ( Date: Mon, 11 Mar 2024 21:05:27 +0530 Subject: [PATCH 16/24] fix: updated extensions in read only instance of the editor (#3925) --- .../editor/core/src/ui/extensions/index.tsx | 3 +- .../core/src/ui/read-only/extensions.tsx | 43 ++++++++++--------- .../document-editor/src/ui/readonly/index.tsx | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/editor/core/src/ui/extensions/index.tsx b/packages/editor/core/src/ui/extensions/index.tsx index 7da381e98..1a932d6d5 100644 --- a/packages/editor/core/src/ui/extensions/index.tsx +++ b/packages/editor/core/src/ui/extensions/index.tsx @@ -27,7 +27,7 @@ import { RestoreImage } from "src/types/restore-image"; import { CustomLinkExtension } from "src/ui/extensions/custom-link"; import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; import { CustomTypographyExtension } from "src/ui/extensions/typography"; -import { CustomHorizontalRule } from "./horizontal-rule/horizontal-rule"; +import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; export const CoreEditorExtensions = ( mentionConfig: { @@ -66,7 +66,6 @@ export const CoreEditorExtensions = ( CustomQuoteExtension.configure({ HTMLAttributes: { className: "border-l-4 border-custom-border-300" }, }), - CustomHorizontalRule.configure({ HTMLAttributes: { class: "mt-4 mb-4" }, }), diff --git a/packages/editor/core/src/ui/read-only/extensions.tsx b/packages/editor/core/src/ui/read-only/extensions.tsx index cf7c4ee18..93e1b3887 100644 --- a/packages/editor/core/src/ui/read-only/extensions.tsx +++ b/packages/editor/core/src/ui/read-only/extensions.tsx @@ -5,7 +5,6 @@ import { Color } from "@tiptap/extension-color"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Markdown } from "tiptap-markdown"; -import Gapcursor from "@tiptap/extension-gapcursor"; import { TableHeader } from "src/ui/extensions/table/table-header/table-header"; import { Table } from "src/ui/extensions/table/table"; @@ -17,6 +16,11 @@ import { isValidHttpUrl } from "src/lib/utils"; import { Mentions } from "src/ui/mentions"; import { IMentionSuggestion } from "src/types/mention-suggestion"; import { CustomLinkExtension } from "src/ui/extensions/custom-link"; +import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule"; +import { CustomQuoteExtension } from "src/ui/extensions/quote"; +import { CustomTypographyExtension } from "src/ui/extensions/typography"; +import { CustomCodeBlockExtension } from "src/ui/extensions/code"; +import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline"; export const CoreReadOnlyEditorExtensions = (mentionConfig: { mentionSuggestions: IMentionSuggestion[]; @@ -38,36 +42,31 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { class: "leading-normal -mb-2", }, }, - blockquote: { - HTMLAttributes: { - class: "border-l-4 border-custom-border-300", - }, - }, - code: { - HTMLAttributes: { - class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000", - spellcheck: "false", - }, - }, + code: false, codeBlock: false, - horizontalRule: { - HTMLAttributes: { class: "mt-4 mb-4" }, - }, - dropcursor: { - color: "rgba(var(--color-text-100))", - width: 2, - }, + horizontalRule: false, + blockquote: false, + dropcursor: false, gapcursor: false, }), - Gapcursor, + CustomQuoteExtension.configure({ + HTMLAttributes: { className: "border-l-4 border-custom-border-300" }, + }), + CustomHorizontalRule.configure({ + HTMLAttributes: { class: "mt-4 mb-4" }, + }), CustomLinkExtension.configure({ + openOnClick: true, + autolink: true, + linkOnPaste: true, protocols: ["http", "https"], - validate: (url) => isValidHttpUrl(url), + validate: (url: string) => isValidHttpUrl(url), HTMLAttributes: { class: "text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer", }, }), + CustomTypographyExtension, ReadOnlyImageExtension.configure({ HTMLAttributes: { class: "rounded-lg border border-custom-border-300", @@ -87,6 +86,8 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: { }, nested: true, }), + CustomCodeBlockExtension, + CustomCodeInlineExtension, Markdown.configure({ html: true, transformCopiedText: true, diff --git a/packages/editor/document-editor/src/ui/readonly/index.tsx b/packages/editor/document-editor/src/ui/readonly/index.tsx index ba0c760bf..22099281e 100644 --- a/packages/editor/document-editor/src/ui/readonly/index.tsx +++ b/packages/editor/document-editor/src/ui/readonly/index.tsx @@ -113,7 +113,7 @@ const DocumentReadOnlyEditor = ({ tabIndex={tabIndex} onActionCompleteHandler={onActionCompleteHandler} updatePageTitle={() => Promise.resolve()} - readonly={true} + readonly editor={editor} editorClassNames={editorClassNames} documentDetails={documentDetails} From 4a06572f7311f7083fcdad6999b79e18520abe8a Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:08:10 +0530 Subject: [PATCH 17/24] chore: remove deprecated envs (#3927) --- apiserver/.env.example | 19 ------------------- deploy/selfhost/docker-compose.yml | 28 ++++------------------------ deploy/selfhost/variables.env | 19 +------------------ 3 files changed, 5 insertions(+), 61 deletions(-) diff --git a/apiserver/.env.example b/apiserver/.env.example index 42b0e32e5..97dc4dda8 100644 --- a/apiserver/.env.example +++ b/apiserver/.env.example @@ -14,10 +14,6 @@ POSTGRES_HOST="plane-db" POSTGRES_DB="plane" DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB} -# Oauth variables -GOOGLE_CLIENT_ID="" -GITHUB_CLIENT_ID="" -GITHUB_CLIENT_SECRET="" # Redis Settings REDIS_HOST="plane-redis" @@ -34,11 +30,6 @@ AWS_S3_BUCKET_NAME="uploads" # Maximum file upload limit FILE_SIZE_LIMIT=5242880 -# GPT settings -OPENAI_API_BASE="https://api.openai.com/v1" # deprecated -OPENAI_API_KEY="sk-" # deprecated -GPT_ENGINE="gpt-3.5-turbo" # deprecated - # Settings related to Docker DOCKERIZED=1 # deprecated @@ -48,16 +39,6 @@ USE_MINIO=1 # Nginx Configuration NGINX_PORT=80 - -# SignUps -ENABLE_SIGNUP="1" - -# Enable Email/Password Signup -ENABLE_EMAIL_PASSWORD="1" - -# Enable Magic link Login -ENABLE_MAGIC_LINK_LOGIN="0" - # Email redirections and minio domain settings WEB_URL="http://localhost" diff --git a/deploy/selfhost/docker-compose.yml b/deploy/selfhost/docker-compose.yml index 07e5ea9f6..3af4b8449 100644 --- a/deploy/selfhost/docker-compose.yml +++ b/deploy/selfhost/docker-compose.yml @@ -1,18 +1,12 @@ version: "3.8" -x-app-env : &app-env +x-app-env: &app-env environment: - NGINX_PORT=${NGINX_PORT:-80} - WEB_URL=${WEB_URL:-http://localhost} - DEBUG=${DEBUG:-0} - - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-plane.settings.production} # deprecated - - NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL:-http://localhost/spaces} # deprecated - SENTRY_DSN=${SENTRY_DSN:-""} - SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"} - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-""} - - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-""} - - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-""} - - DOCKERIZED=${DOCKERIZED:-1} # deprecated - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""} # Gunicorn Workers - GUNICORN_WORKERS=${GUNICORN_WORKERS:-2} @@ -28,20 +22,6 @@ x-app-env : &app-env - REDIS_HOST=${REDIS_HOST:-plane-redis} - REDIS_PORT=${REDIS_PORT:-6379} - REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/} - # EMAIL SETTINGS - Deprecated can be configured through admin panel - - EMAIL_HOST=${EMAIL_HOST:-""} - - EMAIL_HOST_USER=${EMAIL_HOST_USER:-""} - - EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-""} - - EMAIL_PORT=${EMAIL_PORT:-587} - - EMAIL_FROM=${EMAIL_FROM:-"Team Plane "} - - EMAIL_USE_TLS=${EMAIL_USE_TLS:-1} - - EMAIL_USE_SSL=${EMAIL_USE_SSL:-0} - - DEFAULT_EMAIL=${DEFAULT_EMAIL:-captain@plane.so} - - DEFAULT_PASSWORD=${DEFAULT_PASSWORD:-password123} - # LOGIN/SIGNUP SETTINGS - Deprecated can be configured through admin panel - - ENABLE_SIGNUP=${ENABLE_SIGNUP:-1} - - ENABLE_EMAIL_PASSWORD=${ENABLE_EMAIL_PASSWORD:-1} - - ENABLE_MAGIC_LINK_LOGIN=${ENABLE_MAGIC_LINK_LOGIN:-0} # Application secret - SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5} # DATA STORE SETTINGS @@ -122,8 +102,8 @@ services: pull_policy: ${PULL_POLICY:-always} restart: no command: > - sh -c "python manage.py wait_for_db && - python manage.py migrate" + sh -c "python manage.py wait_for_db && + python manage.py migrate" depends_on: - plane-db - plane-redis @@ -159,7 +139,7 @@ services: image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-latest} pull_policy: ${PULL_POLICY:-always} ports: - - ${NGINX_PORT}:80 + - ${NGINX_PORT}:80 depends_on: - web - api diff --git a/deploy/selfhost/variables.env b/deploy/selfhost/variables.env index 6d2cde0ff..9a755d012 100644 --- a/deploy/selfhost/variables.env +++ b/deploy/selfhost/variables.env @@ -7,13 +7,8 @@ API_REPLICAS=1 NGINX_PORT=80 WEB_URL=http://localhost DEBUG=0 -NEXT_PUBLIC_DEPLOY_URL=http://localhost/spaces SENTRY_DSN= SENTRY_ENVIRONMENT=production -GOOGLE_CLIENT_ID= -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -DOCKERIZED=1 # deprecated CORS_ALLOWED_ORIGINS=http://localhost #DB SETTINGS @@ -30,19 +25,7 @@ REDIS_HOST=plane-redis REDIS_PORT=6379 REDIS_URL=redis://${REDIS_HOST}:6379/ -# EMAIL SETTINGS -EMAIL_HOST= -EMAIL_HOST_USER= -EMAIL_HOST_PASSWORD= -EMAIL_PORT=587 -EMAIL_FROM=Team Plane -EMAIL_USE_TLS=1 -EMAIL_USE_SSL=0 - -# LOGIN/SIGNUP SETTINGS -ENABLE_SIGNUP=1 -ENABLE_EMAIL_PASSWORD=1 -ENABLE_MAGIC_LINK_LOGIN=0 +# Secret Key SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5 # DATA STORE SETTINGS From b2146098e24a9097bf9cab8cc979dbd3762e6a3e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:09:16 +0530 Subject: [PATCH 18/24] chore: added view less button to the collaborators widget (#3928) --- .../recent-collaborators/default-list.tsx | 33 ++++++++++++++----- .../widgets/recent-collaborators/root.tsx | 4 +-- .../recent-collaborators/search-list.tsx | 33 ++++++++++++++----- .../project/send-project-invitation-modal.tsx | 8 ++--- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx index a27534bbf..8fb884c9b 100644 --- a/web/components/dashboard/widgets/recent-collaborators/default-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/default-list.tsx @@ -37,21 +37,36 @@ export const DefaultCollaboratorsList: React.FC = (props) => { /> ); + const showViewMoreButton = pageCount < totalPages && resultsCount !== 0; + const showViewLessButton = pageCount > 1; + return ( <>
{collaboratorsPages}
- {pageCount < totalPages && resultsCount !== 0 && ( + {(showViewLessButton || showViewMoreButton) && (
- + {showViewLessButton && ( + + )} + {showViewMoreButton && ( + + )}
)} diff --git a/web/components/dashboard/widgets/recent-collaborators/root.tsx b/web/components/dashboard/widgets/recent-collaborators/root.tsx index d65b15db7..f544e0da6 100644 --- a/web/components/dashboard/widgets/recent-collaborators/root.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/root.tsx @@ -17,9 +17,9 @@ export const RecentCollaboratorsWidget: React.FC = (props) => {
-

Most active members

+

Collaborators

- Top eight active members in your project by last activity + View and find all members you collaborate with across projects

diff --git a/web/components/dashboard/widgets/recent-collaborators/search-list.tsx b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx index 32baa72ad..7323ad944 100644 --- a/web/components/dashboard/widgets/recent-collaborators/search-list.tsx +++ b/web/components/dashboard/widgets/recent-collaborators/search-list.tsx @@ -48,6 +48,9 @@ export const SearchedCollaboratorsList: React.FC = (props) => { /> ); + const showViewMoreButton = pageCount < totalPages && resultsCount !== 0; + const showViewLessButton = pageCount > 1; + const emptyStateImage = resolvedTheme === "dark" ? DarkImage : LightImage; return ( @@ -63,16 +66,28 @@ export const SearchedCollaboratorsList: React.FC = (props) => {

No matching member

)} - {pageCount < totalPages && resultsCount !== 0 && ( + {(showViewLessButton || showViewMoreButton) && (
- + {showViewLessButton && ( + + )} + {showViewMoreButton && ( + + )}
)} diff --git a/web/components/project/send-project-invitation-modal.tsx b/web/components/project/send-project-invitation-modal.tsx index 24fc36521..aa680598e 100644 --- a/web/components/project/send-project-invitation-modal.tsx +++ b/web/components/project/send-project-invitation-modal.tsx @@ -142,8 +142,9 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { if (!memberDetails?.member) return; return { value: `${memberDetails?.member.id}`, - query: `${memberDetails?.member.first_name} ${memberDetails?.member - .last_name} ${memberDetails?.member.display_name.toLowerCase()}`, + query: `${memberDetails?.member.first_name} ${ + memberDetails?.member.last_name + } ${memberDetails?.member.display_name.toLowerCase()}`, content: (
@@ -211,9 +212,6 @@ export const SendProjectInvitationModal: React.FC = observer((props) => { rules={{ required: "Please select a member" }} render={({ field: { value, onChange } }) => { const selectedMember = getWorkspaceMemberDetails(value); - - if (!selectedMember?.member) return <>; - return ( Date: Mon, 11 Mar 2024 21:09:41 +0530 Subject: [PATCH 19/24] chore: profile setting scroll fix and scrollbar added in activity section (#3929) --- web/layouts/settings-layout/profile/layout.tsx | 2 +- web/layouts/settings-layout/profile/preferences/layout.tsx | 4 +--- web/pages/profile/activity.tsx | 2 +- web/pages/profile/preferences/email.tsx | 2 +- web/pages/profile/preferences/theme.tsx | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/web/layouts/settings-layout/profile/layout.tsx b/web/layouts/settings-layout/profile/layout.tsx index 67f545a2d..ef778cacb 100644 --- a/web/layouts/settings-layout/profile/layout.tsx +++ b/web/layouts/settings-layout/profile/layout.tsx @@ -21,7 +21,7 @@ export const ProfileSettingsLayout: FC = (props) => {
{header} -
{children}
+
{children}
diff --git a/web/layouts/settings-layout/profile/preferences/layout.tsx b/web/layouts/settings-layout/profile/preferences/layout.tsx index 71a1fdd85..09cbd9649 100644 --- a/web/layouts/settings-layout/profile/preferences/layout.tsx +++ b/web/layouts/settings-layout/profile/preferences/layout.tsx @@ -73,9 +73,7 @@ export const ProfilePreferenceSettingsLayout: FC
{header} -
- {children} -
+
{children}
diff --git a/web/pages/profile/activity.tsx b/web/pages/profile/activity.tsx index bda1295cf..17ad3292c 100644 --- a/web/pages/profile/activity.tsx +++ b/web/pages/profile/activity.tsx @@ -49,7 +49,7 @@ const ProfileActivityPage: NextPageWithLayout = observer(() => { themeStore.toggleSidebar()} />

Activity

-
+
{activityPages} {pageCount < totalPages && resultsCount !== 0 && (
diff --git a/web/pages/profile/preferences/email.tsx b/web/pages/profile/preferences/email.tsx index b34a493e5..923ba70d9 100644 --- a/web/pages/profile/preferences/email.tsx +++ b/web/pages/profile/preferences/email.tsx @@ -28,7 +28,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = () => { return ( <> -
+
diff --git a/web/pages/profile/preferences/theme.tsx b/web/pages/profile/preferences/theme.tsx index e23e94c66..49aa2a777 100644 --- a/web/pages/profile/preferences/theme.tsx +++ b/web/pages/profile/preferences/theme.tsx @@ -54,7 +54,7 @@ const ProfilePreferencesThemePage: NextPageWithLayout = observer(() => { <> {currentUser ? ( -
+

Preferences

From e3ac075ee2cdc96acd16e7590614d92be3784483 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:12:28 +0530 Subject: [PATCH 20/24] chore: error state for the issues widgets (#3934) --- .../dashboard/widgets/assigned-issues.tsx | 146 ++++++++++-------- .../dashboard/widgets/created-issues.tsx | 146 ++++++++++-------- .../dashboard/widgets/error-states/index.ts | 1 + .../dashboard/widgets/error-states/issues.tsx | 32 ++++ web/components/dashboard/widgets/index.ts | 1 + web/store/dashboard.store.ts | 86 +++++++---- 6 files changed, 256 insertions(+), 156 deletions(-) create mode 100644 web/components/dashboard/widgets/error-states/index.ts create mode 100644 web/components/dashboard/widgets/error-states/issues.tsx diff --git a/web/components/dashboard/widgets/assigned-issues.tsx b/web/components/dashboard/widgets/assigned-issues.tsx index 1e031cacd..cd4115ac7 100644 --- a/web/components/dashboard/widgets/assigned-issues.tsx +++ b/web/components/dashboard/widgets/assigned-issues.tsx @@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, + IssuesErrorState, TabsList, WidgetIssuesList, WidgetLoader, @@ -26,10 +27,12 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { // states const [fetching, setFetching] = useState(false); // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; @@ -73,74 +76,91 @@ export const AssignedIssuesWidget: React.FC = observer((props) => { const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - if (!widgetDetails || !widgetStats) return ; + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; return (
-
- - Assigned to you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming"; - + {widgetStatsError ? ( + handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} + duration: EDurationFilters.NONE, + tab: "pending", + }) + } /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); - }} - className="h-full flex flex-col" - > -
- -
- - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; + ) : ( + widgetStats && ( + <> +
+ + Assigned to you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } - return ( - - - - ); - })} - - + if (val === selectedDurationFilter) return; + + let newTab = selectedTab; + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") newTab = "pending"; + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") + newTab = "upcoming"; + + handleUpdateFilters({ + duration: val, + tab: newTab, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab?.key ?? "completed" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + +
+ + ) + )}
); }); diff --git a/web/components/dashboard/widgets/created-issues.tsx b/web/components/dashboard/widgets/created-issues.tsx index d36260f21..2b2954018 100644 --- a/web/components/dashboard/widgets/created-issues.tsx +++ b/web/components/dashboard/widgets/created-issues.tsx @@ -7,6 +7,7 @@ import { useDashboard } from "hooks/store"; // components import { DurationFilterDropdown, + IssuesErrorState, TabsList, WidgetIssuesList, WidgetLoader, @@ -26,10 +27,12 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { // states const [fetching, setFetching] = useState(false); // store hooks - const { fetchWidgetStats, getWidgetDetails, getWidgetStats, updateDashboardWidgetFilters } = useDashboard(); + const { fetchWidgetStats, getWidgetDetails, getWidgetStats, getWidgetStatsError, updateDashboardWidgetFilters } = + useDashboard(); // derived values const widgetDetails = getWidgetDetails(workspaceSlug, dashboardId, WIDGET_KEY); const widgetStats = getWidgetStats(workspaceSlug, dashboardId, WIDGET_KEY); + const widgetStatsError = getWidgetStatsError(workspaceSlug, dashboardId, WIDGET_KEY); const selectedDurationFilter = widgetDetails?.widget_filters.duration ?? EDurationFilters.NONE; const selectedTab = getTabKey(selectedDurationFilter, widgetDetails?.widget_filters.tab); const selectedCustomDates = widgetDetails?.widget_filters.custom_dates ?? []; @@ -70,74 +73,91 @@ export const CreatedIssuesWidget: React.FC = observer((props) => { const tabsList = selectedDurationFilter === "none" ? UNFILTERED_ISSUES_TABS_LIST : FILTERED_ISSUES_TABS_LIST; const selectedTabIndex = tabsList.findIndex((tab) => tab.key === selectedTab); - if (!widgetDetails || !widgetStats) return ; + if ((!widgetDetails || !widgetStats) && !widgetStatsError) return ; return (
-
- - Created by you - - { - if (val === "custom" && customDates) { - handleUpdateFilters({ - duration: val, - custom_dates: customDates, - }); - return; - } - - if (val === selectedDurationFilter) return; - - let newTab = selectedTab; - // switch to pending tab if target date is changed to none - if (val === "none" && selectedTab !== "completed") newTab = "pending"; - // switch to upcoming tab if target date is changed to other than none - if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") newTab = "upcoming"; - + {widgetStatsError ? ( + handleUpdateFilters({ - duration: val, - tab: newTab, - }); - }} + duration: EDurationFilters.NONE, + tab: "pending", + }) + } /> -
- { - const newSelectedTab = tabsList[i]; - handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); - }} - className="h-full flex flex-col" - > -
- -
- - {tabsList.map((tab) => { - if (tab.key !== selectedTab) return null; + ) : ( + widgetStats && ( + <> +
+ + Created by you + + { + if (val === "custom" && customDates) { + handleUpdateFilters({ + duration: val, + custom_dates: customDates, + }); + return; + } - return ( - - - - ); - })} - - + if (val === selectedDurationFilter) return; + + let newTab = selectedTab; + // switch to pending tab if target date is changed to none + if (val === "none" && selectedTab !== "completed") newTab = "pending"; + // switch to upcoming tab if target date is changed to other than none + if (val !== "none" && selectedDurationFilter === "none" && selectedTab !== "completed") + newTab = "upcoming"; + + handleUpdateFilters({ + duration: val, + tab: newTab, + }); + }} + /> +
+ { + const newSelectedTab = tabsList[i]; + handleUpdateFilters({ tab: newSelectedTab.key ?? "completed" }); + }} + className="h-full flex flex-col" + > +
+ +
+ + {tabsList.map((tab) => { + if (tab.key !== selectedTab) return null; + + return ( + + + + ); + })} + +
+ + ) + )}
); }); diff --git a/web/components/dashboard/widgets/error-states/index.ts b/web/components/dashboard/widgets/error-states/index.ts new file mode 100644 index 000000000..bd8854f37 --- /dev/null +++ b/web/components/dashboard/widgets/error-states/index.ts @@ -0,0 +1 @@ +export * from "./issues"; diff --git a/web/components/dashboard/widgets/error-states/issues.tsx b/web/components/dashboard/widgets/error-states/issues.tsx new file mode 100644 index 000000000..6cfce13b4 --- /dev/null +++ b/web/components/dashboard/widgets/error-states/issues.tsx @@ -0,0 +1,32 @@ +import { AlertTriangle, RefreshCcw } from "lucide-react"; +// ui +import { Button } from "@plane/ui"; + +type Props = { + isRefreshing: boolean; + onClick: () => void; +}; + +export const IssuesErrorState: React.FC = (props) => { + const { isRefreshing, onClick } = props; + + return ( +
+
+
+ +
+

There was an error in fetching widget details

+ +
+
+ ); +}; diff --git a/web/components/dashboard/widgets/index.ts b/web/components/dashboard/widgets/index.ts index a481a8881..31fc645d4 100644 --- a/web/components/dashboard/widgets/index.ts +++ b/web/components/dashboard/widgets/index.ts @@ -1,5 +1,6 @@ export * from "./dropdowns"; export * from "./empty-states"; +export * from "./error-states"; export * from "./issue-panels"; export * from "./loaders"; export * from "./assigned-issues"; diff --git a/web/store/dashboard.store.ts b/web/store/dashboard.store.ts index c8a07428e..4eaf325b2 100644 --- a/web/store/dashboard.store.ts +++ b/web/store/dashboard.store.ts @@ -15,6 +15,8 @@ import { } from "@plane/types"; export interface IDashboardStore { + // error states + widgetStatsError: { [workspaceSlug: string]: Record> }; // observables homeDashboardId: string | null; widgetDetails: { [workspaceSlug: string]: Record }; @@ -36,6 +38,7 @@ export interface IDashboardStore { // computed actions getWidgetDetails: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => TWidget | undefined; getWidgetStats: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => T | undefined; + getWidgetStatsError: (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => any | null; // actions fetchHomeDashboardWidgets: (workspaceSlug: string) => Promise; fetchWidgetStats: ( @@ -58,6 +61,8 @@ export interface IDashboardStore { } export class DashboardStore implements IDashboardStore { + // error states + widgetStatsError: { [workspaceSlug: string]: Record> } = {}; // observables homeDashboardId: string | null = null; widgetDetails: { [workspaceSlug: string]: Record } = {}; @@ -70,6 +75,8 @@ export class DashboardStore implements IDashboardStore { constructor(_rootStore: RootStore) { makeObservable(this, { + // error states + widgetStatsError: observable, // observables homeDashboardId: observable.ref, widgetDetails: observable, @@ -93,7 +100,7 @@ export class DashboardStore implements IDashboardStore { /** * @description get home dashboard widgets - * @returns home dashboard widgets + * @returns {TWidget[] | undefined} */ get homeDashboardWidgets() { const workspaceSlug = this.routerStore.workspaceSlug; @@ -104,10 +111,10 @@ export class DashboardStore implements IDashboardStore { /** * @description get widget details - * @param workspaceSlug - * @param dashboardId - * @param widgetId - * @returns widget details + * @param {string} workspaceSlug + * @param {string} dashboardId + * @param {TWidgetKeys} widgetKey + * @returns {TWidget | undefined} */ getWidgetDetails = computedFn((workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => { const widgets = this.widgetDetails?.[workspaceSlug]?.[dashboardId]; @@ -117,20 +124,30 @@ export class DashboardStore implements IDashboardStore { /** * @description get widget stats - * @param workspaceSlug - * @param dashboardId - * @param widgetKey - * @returns widget stats + * @param {string} workspaceSlug + * @param {string} dashboardId + * @param {TWidgetKeys} widgetKey + * @returns {T | undefined} */ getWidgetStats = (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys): T | undefined => (this.widgetStats?.[workspaceSlug]?.[dashboardId]?.[widgetKey] as unknown as T) ?? undefined; /** - * @description fetch home dashboard details and widgets - * @param workspaceSlug - * @returns home dashboard response + * @description get widget stats error + * @param {string} workspaceSlug + * @param {string} dashboardId + * @param {TWidgetKeys} widgetKey + * @returns {any | null} */ - fetchHomeDashboardWidgets = async (workspaceSlug: string) => { + getWidgetStatsError = (workspaceSlug: string, dashboardId: string, widgetKey: TWidgetKeys) => + this.widgetStatsError?.[workspaceSlug]?.[dashboardId]?.[widgetKey] ?? null; + + /** + * @description fetch home dashboard details and widgets + * @param {string} workspaceSlug + * @returns {Promise} + */ + fetchHomeDashboardWidgets = async (workspaceSlug: string): Promise => { try { const response = await this.dashboardService.getHomeDashboardWidgets(workspaceSlug); @@ -151,27 +168,36 @@ export class DashboardStore implements IDashboardStore { /** * @description fetch widget stats - * @param workspaceSlug - * @param dashboardId - * @param widgetKey + * @param {string} workspaceSlug + * @param {string} dashboardId + * @param {TWidgetStatsRequestParams} widgetKey * @returns widget stats */ fetchWidgetStats = async (workspaceSlug: string, dashboardId: string, params: TWidgetStatsRequestParams) => - this.dashboardService.getWidgetStats(workspaceSlug, dashboardId, params).then((res) => { - runInAction(() => { - // @ts-ignore - if (res.issues) this.issueStore.addIssue(res.issues); - set(this.widgetStats, [workspaceSlug, dashboardId, params.widget_key], res); - }); + this.dashboardService + .getWidgetStats(workspaceSlug, dashboardId, params) + .then((res) => { + runInAction(() => { + // @ts-ignore + if (res.issues) this.issueStore.addIssue(res.issues); + set(this.widgetStats, [workspaceSlug, dashboardId, params.widget_key], res); + set(this.widgetStatsError, [workspaceSlug, dashboardId, params.widget_key], null); + }); + return res; + }) + .catch((error) => { + runInAction(() => { + set(this.widgetStatsError, [workspaceSlug, dashboardId, params.widget_key], error); + }); - return res; - }); + throw error; + }); /** * @description update dashboard widget - * @param dashboardId - * @param widgetId - * @param data + * @param {string} dashboardId + * @param {string} widgetId + * @param {Partial} data * @returns updated widget */ updateDashboardWidget = async ( @@ -209,9 +235,9 @@ export class DashboardStore implements IDashboardStore { /** * @description update dashboard widget filters - * @param dashboardId - * @param widgetId - * @param data + * @param {string} dashboardId + * @param {string} widgetId + * @param {TWidgetFiltersFormData} data * @returns updated widget */ updateDashboardWidgetFilters = async ( From 9c29ad1a28529312c33be11d3fa963c827ce6754 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:12:59 +0530 Subject: [PATCH 21/24] chore: list layout scrollbar improvement (#3933) --- web/components/issues/issue-layouts/list/default.tsx | 5 ++++- web/styles/globals.css | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 12179ee97..4009df6d2 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -123,7 +123,10 @@ const GroupByList: React.FC = (props) => { const isGroupByCreatedBy = group_by === "created_by"; return ( -
+
{groups && groups.length > 0 && groups.map( diff --git a/web/styles/globals.css b/web/styles/globals.css index 6c51e75c4..5b24abc08 100644 --- a/web/styles/globals.css +++ b/web/styles/globals.css @@ -602,6 +602,9 @@ div.web-view-spinner div.bar12 { .horizontal-scrollbar::-webkit-scrollbar-corner { background-color: transparent; } +.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track { + margin-top: 44px; +} /* scrollbar sm size */ .scrollbar-sm::-webkit-scrollbar { From 48c9b78397c3e025cb68b31ee4bf3c29c0e8b539 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Mon, 11 Mar 2024 21:23:09 +0530 Subject: [PATCH 22/24] [WEB-545] chore: completed cycle empty state (#3931) * chore: completed cycle issue transfer empty state added * chore: cycle empty state variable updated * chore: empty state config file updated --- web/components/empty-state/empty-state.tsx | 4 +- .../issue-layouts/empty-states/cycle.tsx | 28 ++- web/constants/empty-state.ts | 188 +++++++++--------- .../cycle/completed-no-issues-dark.webp | Bin 0 -> 88100 bytes .../cycle/completed-no-issues-light.webp | Bin 0 -> 90250 bytes 5 files changed, 120 insertions(+), 100 deletions(-) create mode 100644 web/public/empty-state/cycle/completed-no-issues-dark.webp create mode 100644 web/public/empty-state/cycle/completed-no-issues-light.webp diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index a9ff4decd..e718c065a 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -9,12 +9,12 @@ import { useUser } from "hooks/store"; import { Button, TButtonVariant } from "@plane/ui"; import { ComicBoxButton } from "./comic-box-button"; // constant -import { EMPTY_STATE_DETAILS, EmptyStateKeys } from "constants/empty-state"; +import { EMPTY_STATE_DETAILS, EmptyStateType } from "constants/empty-state"; // helpers import { cn } from "helpers/common.helper"; export type EmptyStateProps = { - type: EmptyStateKeys; + type: EmptyStateType; size?: "sm" | "md" | "lg"; layout?: "widget-simple" | "screen-detailed" | "screen-simple"; additionalPath?: string; diff --git a/web/components/issues/issue-layouts/empty-states/cycle.tsx b/web/components/issues/issue-layouts/empty-states/cycle.tsx index 350e4dbb4..1a49794c6 100644 --- a/web/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/components/issues/issue-layouts/empty-states/cycle.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; import { observer } from "mobx-react-lite"; +import isEmpty from "lodash/isEmpty"; // hooks -import { useApplication, useEventTracker, useIssues } from "hooks/store"; +import { useApplication, useCycle, useEventTracker, useIssues } from "hooks/store"; // ui import { TOAST_TYPE, setToast } from "@plane/ui"; import { ExistingIssuesListModal } from "components/core"; @@ -27,12 +28,15 @@ export const CycleEmptyState: React.FC = observer((props) => { // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); // store hooks + const { getCycleById } = useCycle(); const { issues } = useIssues(EIssuesStoreType.CYCLE); const { commandPalette: { toggleCreateIssueModal }, } = useApplication(); const { setTrackElement } = useEventTracker(); + const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; + const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -56,8 +60,14 @@ export const CycleEmptyState: React.FC = observer((props) => { ); }; - const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; - const additionalPath = activeLayout ?? "list"; + const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); + + const emptyStateType = isCompletedCycleSnapshotAvailable + ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES + : isEmptyFilters + ? EmptyStateType.PROJECT_EMPTY_FILTER + : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; + const additionalPath = isCompletedCycleSnapshotAvailable ? undefined : activeLayout ?? "list"; const emptyStateSize = isEmptyFilters ? "lg" : "sm"; return ( @@ -76,14 +86,18 @@ export const CycleEmptyState: React.FC = observer((props) => { additionalPath={additionalPath} size={emptyStateSize} primaryButtonOnClick={ - isEmptyFilters - ? undefined - : () => { + !isCompletedCycleSnapshotAvailable && !isEmptyFilters + ? () => { setTrackElement("Cycle issue empty state"); toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); } + : undefined + } + secondaryButtonOnClick={ + !isCompletedCycleSnapshotAvailable && isEmptyFilters + ? handleClearAllFilters + : () => setCycleIssuesListModal(true) } - secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true)} />
diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index 495bff29f..dd6d76ef3 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -2,7 +2,7 @@ import { EUserProjectRoles } from "./project"; import { EUserWorkspaceRoles } from "./workspace"; export interface EmptyStateDetails { - key: string; + key: EmptyStateType; title?: string; description?: string; path?: string; @@ -26,8 +26,6 @@ export interface EmptyStateDetails { access?: EUserWorkspaceRoles | EUserProjectRoles; } -export type EmptyStateKeys = keyof typeof emptyStateDetails; - export enum EmptyStateType { WORKSPACE_DASHBOARD = "workspace-dashboard", WORKSPACE_ANALYTICS = "workspace-analytics", @@ -52,6 +50,7 @@ export enum EmptyStateType { PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", PROJECT_CYCLE_ACTIVE = "project-cycle-active", PROJECT_CYCLE_ALL = "project-cycle-all", + PROJECT_CYCLE_COMPLETED_NO_ISSUES = "project-cycle-completed-no-issues", PROJECT_EMPTY_FILTER = "project-empty-filter", PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", @@ -67,7 +66,7 @@ export enum EmptyStateType { PROJECT_VIEW = "project-view", PROJECT_PAGE = "project-page", PROJECT_PAGE_ALL = "project-page-all", - PROJECT_PAGE_FAVORITE = "project-page-favorites", + PROJECT_PAGE_FAVORITES = "project-page-favorites", PROJECT_PAGE_PRIVATE = "project-page-private", PROJECT_PAGE_SHARED = "project-page-shared", PROJECT_PAGE_ARCHIVED = "project-page-archived", @@ -76,8 +75,8 @@ export enum EmptyStateType { const emptyStateDetails = { // workspace - "workspace-dashboard": { - key: "workspace-dashboard", + [EmptyStateType.WORKSPACE_DASHBOARD]: { + key: EmptyStateType.WORKSPACE_DASHBOARD, title: "Overview of your projects, activity, and metrics", description: " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", @@ -94,8 +93,8 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "workspace-analytics": { - key: "workspace-analytics", + [EmptyStateType.WORKSPACE_ANALYTICS]: { + key: EmptyStateType.WORKSPACE_ANALYTICS, title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", description: "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", @@ -111,8 +110,8 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "workspace-projects": { - key: "workspace-projects", + [EmptyStateType.WORKSPACE_PROJECTS]: { + key: EmptyStateType.WORKSPACE_PROJECTS, title: "Start a Project", description: "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal.", @@ -128,8 +127,8 @@ const emptyStateDetails = { access: EUserWorkspaceRoles.MEMBER, }, // all-issues - "workspace-all-issues": { - key: "workspace-all-issues", + [EmptyStateType.WORKSPACE_ALL_ISSUES]: { + key: EmptyStateType.WORKSPACE_ALL_ISSUES, title: "No issues in the project", description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", path: "/empty-state/all-issues/all-issues", @@ -139,8 +138,8 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "workspace-assigned": { - key: "workspace-assigned", + [EmptyStateType.WORKSPACE_ASSIGNED]: { + key: EmptyStateType.WORKSPACE_ASSIGNED, title: "No issues yet", description: "Issues assigned to you can be tracked from here.", path: "/empty-state/all-issues/assigned", @@ -150,8 +149,8 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "workspace-created": { - key: "workspace-created", + [EmptyStateType.WORKSPACE_CREATED]: { + key: EmptyStateType.WORKSPACE_CREATED, title: "No issues yet", description: "All issues created by you come here, track them here directly.", path: "/empty-state/all-issues/created", @@ -161,20 +160,20 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "workspace-subscribed": { - key: "workspace-subscribed", + [EmptyStateType.WORKSPACE_SUBSCRIBED]: { + key: EmptyStateType.WORKSPACE_SUBSCRIBED, title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", path: "/empty-state/all-issues/subscribed", }, - "workspace-custom-view": { - key: "workspace-custom-view", + [EmptyStateType.WORKSPACE_CUSTOM_VIEW]: { + key: EmptyStateType.WORKSPACE_CUSTOM_VIEW, title: "No issues yet", description: "Issues that applies to the filters, track all of them here.", path: "/empty-state/all-issues/custom-view", }, - "workspace-no-projects": { - key: "workspace-no-projects", + [EmptyStateType.WORKSPACE_NO_PROJECTS]: { + key: EmptyStateType.WORKSPACE_NO_PROJECTS, title: "No project", description: "To create issues or manage your work, you need to create a project or be a part of one.", path: "/empty-state/onboarding/projects", @@ -189,72 +188,72 @@ const emptyStateDetails = { access: EUserWorkspaceRoles.MEMBER, }, // workspace settings - "workspace-settings-api-tokens": { - key: "workspace-settings-api-tokens", + [EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS]: { + key: EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS, title: "No API tokens created", description: "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", path: "/empty-state/workspace-settings/api-tokens", }, - "workspace-settings-webhooks": { - key: "workspace-settings-webhooks", + [EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS]: { + key: EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS, title: "No webhooks added", description: "Create webhooks to receive real-time updates and automate actions.", path: "/empty-state/workspace-settings/webhooks", }, - "workspace-settings-export": { - key: "workspace-settings-export", + [EmptyStateType.WORKSPACE_SETTINGS_EXPORT]: { + key: EmptyStateType.WORKSPACE_SETTINGS_EXPORT, title: "No previous exports yet", description: "Anytime you export, you will also have a copy here for reference.", path: "/empty-state/workspace-settings/exports", }, - "workspace-settings-import": { - key: "workspace-settings-import", + [EmptyStateType.WORKSPACE_SETTINGS_IMPORT]: { + key: EmptyStateType.WORKSPACE_SETTINGS_IMPORT, title: "No previous imports yet", description: "Find all your previous imports here and download them.", path: "/empty-state/workspace-settings/imports", }, // profile - "profile-assigned": { - key: "profile-assigned", + [EmptyStateType.PROFILE_ASSIGNED]: { + key: EmptyStateType.PROFILE_ASSIGNED, title: "No issues are assigned to you", description: "Issues assigned to you can be tracked from here.", path: "/empty-state/profile/assigned", }, - "profile-created": { - key: "profile-created", + [EmptyStateType.PROFILE_CREATED]: { + key: EmptyStateType.PROFILE_CREATED, title: "No issues yet", description: "All issues created by you come here, track them here directly.", path: "/empty-state/profile/created", }, - "profile-subscribed": { - key: "profile-subscribed", + [EmptyStateType.PROFILE_SUBSCRIBED]: { + key: EmptyStateType.PROFILE_SUBSCRIBED, title: "No issues yet", description: "Subscribe to issues you are interested in, track all of them here.", path: "/empty-state/profile/subscribed", }, // project settings - "project-settings-labels": { - key: "project-settings-labels", + [EmptyStateType.PROJECT_SETTINGS_LABELS]: { + key: EmptyStateType.PROJECT_SETTINGS_LABELS, title: "No labels yet", description: "Create labels to help organize and filter issues in you project.", path: "/empty-state/project-settings/labels", }, - "project-settings-integrations": { - key: "project-settings-integrations", + [EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS]: { + key: EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS, title: "No integrations configured", description: "Configure GitHub and other integrations to sync your project issues.", path: "/empty-state/project-settings/integrations", }, - "project-settings-estimate": { - key: "project-settings-estimate", + [EmptyStateType.PROJECT_SETTINGS_ESTIMATE]: { + key: EmptyStateType.PROJECT_SETTINGS_ESTIMATE, title: "No estimates added", description: "Create a set of estimates to communicate the amount of work per issue.", path: "/empty-state/project-settings/estimates", }, // project cycles - "project-cycles": { - key: "project-cycles", + [EmptyStateType.PROJECT_CYCLES]: { + key: EmptyStateType.PROJECT_CYCLES, title: "Group and timebox your work in Cycles.", description: "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", @@ -270,8 +269,8 @@ const emptyStateDetails = { accessType: "workspace", access: EUserWorkspaceRoles.MEMBER, }, - "project-cycle-no-issues": { - key: "project-cycle-no-issues", + [EmptyStateType.PROJECT_CYCLE_NO_ISSUES]: { + key: EmptyStateType.PROJECT_CYCLE_NO_ISSUES, title: "No issues added to the cycle", description: "Add or create issues you wish to timebox and deliver within this cycle", path: "/empty-state/cycle-issues/", @@ -284,23 +283,30 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-cycle-active": { - key: "project-cycle-active", + [EmptyStateType.PROJECT_CYCLE_ACTIVE]: { + key: EmptyStateType.PROJECT_CYCLE_ACTIVE, title: "No active cycle", description: "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", path: "/empty-state/cycle/active", }, - "project-cycle-all": { - key: "project-cycle-all", + [EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES]: { + key: EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES, + title: "No issues in the cycle", + description: + "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly.", + path: "/empty-state/cycle/completed-no-issues", + }, + [EmptyStateType.PROJECT_CYCLE_ALL]: { + key: EmptyStateType.PROJECT_CYCLE_ALL, title: "No cycles", description: "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", path: "/empty-state/cycle/active", }, // empty filters - "project-empty-filter": { - key: "project-empty-filter", + [EmptyStateType.PROJECT_EMPTY_FILTER]: { + key: EmptyStateType.PROJECT_EMPTY_FILTER, title: "No issues found matching the filters applied", path: "/empty-state/empty-filters/", secondaryButton: { @@ -309,8 +315,8 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-archived-empty-filter": { - key: "project-archived-empty-filter", + [EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER]: { + key: EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER, title: "No issues found matching the filters applied", path: "/empty-state/empty-filters/", secondaryButton: { @@ -319,8 +325,8 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-draft-empty-filter": { - key: "project-draft-empty-filter", + [EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER]: { + key: EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER, title: "No issues found matching the filters applied", path: "/empty-state/empty-filters/", secondaryButton: { @@ -330,8 +336,8 @@ const emptyStateDetails = { access: EUserProjectRoles.MEMBER, }, // project issues - "project-no-issues": { - key: "project-no-issues", + [EmptyStateType.PROJECT_NO_ISSUES]: { + key: EmptyStateType.PROJECT_NO_ISSUES, title: "Create an issue and assign it to someone, even yourself", description: "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", @@ -347,8 +353,8 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-archived-no-issues": { - key: "project-archived-no-issues", + [EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES]: { + key: EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES, title: "No archived issues yet", description: "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", @@ -359,39 +365,39 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-draft-no-issues": { - key: "project-draft-no-issues", + [EmptyStateType.PROJECT_DRAFT_NO_ISSUES]: { + key: EmptyStateType.PROJECT_DRAFT_NO_ISSUES, title: "No draft issues yet", description: "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", path: "/empty-state/draft/draft-issues-empty", }, - "views-empty-search": { - key: "views-empty-search", + [EmptyStateType.VIEWS_EMPTY_SEARCH]: { + key: EmptyStateType.VIEWS_EMPTY_SEARCH, title: "No matching views", description: "No views match the search criteria. Create a new view instead.", path: "/empty-state/search/search", }, - "projects-empty-search": { - key: "projects-empty-search", + [EmptyStateType.PROJECTS_EMPTY_SEARCH]: { + key: EmptyStateType.PROJECTS_EMPTY_SEARCH, title: "No matching projects", description: "No projects detected with the matching criteria. Create a new project instead.", path: "/empty-state/search/project", }, - "commandK-empty-search": { - key: "commandK-empty-search", + [EmptyStateType.COMMANDK_EMPTY_SEARCH]: { + key: EmptyStateType.COMMANDK_EMPTY_SEARCH, title: "No results found. ", path: "/empty-state/search/search", }, - "members-empty-search": { - key: "members-empty-search", + [EmptyStateType.MEMBERS_EMPTY_SEARCH]: { + key: EmptyStateType.MEMBERS_EMPTY_SEARCH, title: "No matching members", description: "Add them to the project if they are already a part of the workspace", path: "/empty-state/search/member", }, // project module - "project-module-issues": { - key: "project-modules-issues", + [EmptyStateType.PROJECT_MODULE_ISSUES]: { + key: EmptyStateType.PROJECT_MODULE_ISSUES, title: "No issues in the module", description: "Create or add issues which you want to accomplish as part of this module", path: "/empty-state/module-issues/", @@ -404,8 +410,8 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-module": { - key: "project-module", + [EmptyStateType.PROJECT_MODULE]: { + key: EmptyStateType.PROJECT_MODULE, title: "Map your project milestones to Modules and track aggregated work easily.", description: "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", @@ -421,8 +427,8 @@ const emptyStateDetails = { access: EUserProjectRoles.MEMBER, }, // project views - "project-view": { - key: "project-view", + [EmptyStateType.PROJECT_VIEW]: { + key: EmptyStateType.PROJECT_VIEW, title: "Save filtered views for your project. Create as many as you need", description: "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", @@ -438,8 +444,8 @@ const emptyStateDetails = { access: EUserProjectRoles.MEMBER, }, // project pages - "project-page": { - key: "pages", + [EmptyStateType.PROJECT_PAGE]: { + key: EmptyStateType.PROJECT_PAGE, title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", description: "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", @@ -455,39 +461,39 @@ const emptyStateDetails = { accessType: "project", access: EUserProjectRoles.MEMBER, }, - "project-page-all": { - key: "project-page-all", + [EmptyStateType.PROJECT_PAGE_ALL]: { + key: EmptyStateType.PROJECT_PAGE_ALL, title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely!", path: "/empty-state/pages/all", }, - "project-page-favorites": { - key: "project-page-favorites", + [EmptyStateType.PROJECT_PAGE_FAVORITES]: { + key: EmptyStateType.PROJECT_PAGE_FAVORITES, title: "No favorite pages yet", description: "Favorites for quick access? mark them and find them right here.", path: "/empty-state/pages/favorites", }, - "project-page-private": { - key: "project-page-private", + [EmptyStateType.PROJECT_PAGE_PRIVATE]: { + key: EmptyStateType.PROJECT_PAGE_PRIVATE, title: "No private pages yet", description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", path: "/empty-state/pages/private", }, - "project-page-shared": { - key: "project-page-shared", + [EmptyStateType.PROJECT_PAGE_SHARED]: { + key: EmptyStateType.PROJECT_PAGE_SHARED, title: "No shared pages yet", description: "See pages shared with everyone in your project right here.", path: "/empty-state/pages/shared", }, - "project-page-archived": { - key: "project-page-archived", + [EmptyStateType.PROJECT_PAGE_ARCHIVED]: { + key: EmptyStateType.PROJECT_PAGE_ARCHIVED, title: "No archived pages yet", description: "Archive pages not on your radar. Access them here when needed.", path: "/empty-state/pages/archived", }, - "project-page-recent": { - key: "project-page-recent", + [EmptyStateType.PROJECT_PAGE_RECENT]: { + key: EmptyStateType.PROJECT_PAGE_RECENT, title: "Write a note, a doc, or a full knowledge base", description: "Pages help you organise your thoughts to create wikis, discussions or even document heated takes for your project. Use it wisely! Pages will be sorted and grouped by last updated", @@ -500,4 +506,4 @@ const emptyStateDetails = { }, } as const; -export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; +export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/public/empty-state/cycle/completed-no-issues-dark.webp b/web/public/empty-state/cycle/completed-no-issues-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..a187dbf5cf83fe78cf14d6af28f3a193c6e4ac62 GIT binary patch literal 88100 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 z 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

Ll>ob5I}7bjra)ldXs??>bpf%2-=J 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 DzI(hp literal 0 HcmV?d00001 diff --git a/web/public/empty-state/cycle/completed-no-issues-light.webp b/web/public/empty-state/cycle/completed-no-issues-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..6009f60f41637c89b67d990fceaaff2c5bf81303 GIT binary patch literal 90250 zcmeFWV{CR0+ma&~~e2j!)It6ij`H>8x=Hb|0Q0{=_gMIOHhW5CJG`Qo(sh;9~ zX|{uJApwOdx7}j1wgEdDP0Pi=kWM6*w*T>K67ER1m`1Bl^$-!8E3j*|$E!ypA_f8f z?xPUak>-6k%CjRA#{DjGasNTlr6JvAPArn5wC9x6gR!&H0C(tS0^Rl zMxOYCMxMxo8F5G#6^&&3bGPI7^Y!vSWas;!_vicL=6^`<*XaMX{D1EKZ~0#){r|xT zzu!a;=on+f9VD)LUP^nfQt-yy&XhMbRk;21_Jwnx0@YsHI zHzN$ANh6VfGSMyaXtIk8*(1liOFifru6p-y;#Q(R$yFvFKc+X%t`9a(jE^-tayPl@ z@cv{~#1*8~|46yZR;KbI)N}>VO3(m(XzAhTQQcX@gefqkJ?97A>I!VhuBdBc>Rw}` zdSzS+3nN-HkkEu}2@*1REb3B@AIx65S{Cz|kmba2rJ0{(vYeWFqYyC(I;<2lE7zZ?|k89>!bO_VG15+fyO!W z*C2ej5j71NhBiwsBud)a6;E2c?dXs{EL^CtsRY1DG4b*$3uQH)sJtAi`m)A z{s;z*v(%_$>u=0T;-RwH`3a1vI3ukLDkz-<4o#rb!mZ_9_2pNzyM8Gypw8XlhA1`1 z%oOA97K6Y8Ez~+JQ#Eq3J=dpsPZ>t6)PrW(t*)4mHxWYljVHCM*ng~sTTpB&s&`#e zBtuIUHV1qDAA|xKq2Gt+Xo$9#Uu@xEQIPjeqDc@FqtTOC`D=R&$QdT0HuP(Mn#ZQL8as9-6^)!%zwOzQX0KC z+l}3Rs@#;p6o_Pyh6FnK-uTt32kYjHI={ql&++4upaVwcH_b=dpT=Pzxh*0=58O16 zx7Drk>K79wL#vVQZ6hlnYNUT8o(Y9(e0I7mynY%S=kCxHSC9tvtbHEtyqMaaKew`-o zxZiAU_fjW=(QP3)ib#~fV&t(`N~K8yM((Q+z+%XwD$9a5Po1Mp@9?5nSDVQ;v02vuWG!kiHR!=JNd^IIg#+q`F?mJztjD9 zk-NK1R8q>?)*-vog8Hqc^9QOjSO-WEb%z8p1fqpA<>VKD$jp?kBccN>3yr7J9l&5I z_7Pxho|HHDb`+g5El9+pE9gn)iq~TA`Ts0e@f9akCq*`T_90bnANoO)o#?(^@0tfA zQla{|PpL?`XhZ5qeJeI<6qEt=v0&{+NMJ1HWNrktAA+L!Q$ z|8ZQ~n_NsR7y=AOyt#!uG-ddvh4>Fl^b5xK2_9p4$siq)_(5bRSYxm(sNp|0R>jbg zw%ms25oAA@@`k_}A^QAetTw}^<0mBaDH~SkaHrH#n+wU^m;4?Zas2c; zEGk5`ECk#_`p}B0Q23kk`Mcw$^Tp}LM&uRD)OrYSAvOg>QTA5jNJAL$yF#toJ!KU$ zCZ!4fh3o#B=~YE8?k)7WMs~Va)GK$jxo0Z*G*DpzfjR5m40p9dj{omRM$_OK{!qB4 zBw(wI_Qv8!k{M2o1S!NeERBmS1O04|SxddmK_>dGhOah}zaSj01u_$UaOy>jLoJpw z^=86;|L?+k;XPQ73Zzle&A7N$^B<;-c+O*i>KEbi=-=~6j{9681**wjY2d$Eq+^)n zVRH|$9V8UrhdK9xlxY%So76g@zPic0C*L{R^E?u~sU$wBo{=q14^uGaw5-x^z~dx0 zRE?yFrBP)RP-6CSa7beo1SlYR0`JIn*)1LSpv9A8n+D^5oTo6r1gI4F!-~W^&>f__ zhM;_il4Nv0wi90s4%N(kMR~kn%0@%c;(UWr?o`f)I>YUFM@14q~pPn=SLMH z1uF2BeGftP#(aM-X{it8UDcCrJ&0Ey4J4WRJ;EH)Lhw!11x*6Xm`w!}Y|kfe{DpCr zS55m$;DR*!TpRyIK;Q9O@>7E*67l_*4#c|G=%2RF)G~l0GXT)NzlBAZPhJz*3 ztp8v#*hhdX{`o_-l|1yHi*eKWT{4iLsRMcTDlrEx${gVlVqIP#PScS!f|Qsr!W<)+ zN^VaCiOSx!Z*!uf(q;j>hE*K&M+1q3Q&N)SC{}3nIPQ`?8kx@w4C(MG+cV0@Scl1<+c1_|Lmoo2 zzoZ$Uch}VE?>0u>3ZC+D?S>CvhNHMG)VcXKG(}#7(~|Z6eK~a`1cBa-@~9;rL1@M` zkP0S{J4rfg1N?IeMu{mXUnQ!v8Q1r>qC?W&O2=@2fqfx5xe{33inu}Sqz88)3*rji zsuc@loMM?K+G7=)X|`QerZ`sxu1LM7RC3 zDV}afCSPvcR$o}!6||u%Xm_9ARgW{XCHmG=k0AIVIY6FT3}Yspedtv7+}8C_!L(x( z@zSozI6qO`o1k4uXxl_T;s$4Tu-f-8-tjW7(91%E;+>~_qzhTskHXr}U$s&)_^Bwv zQHvbSyN?kPpho8ljP3hH!c!qCro=yL4EAH6*WyE!4=5_@`A59icb(IzRcQH`SiMbt z*#0p&S?s+`8bNDQkIPrcU+APFYu(YVSy}x6iS+PlEx81TvXgfX>Qu7@xW|pgJ@z1Y z4#-u?qa1>TPGxV!QUxPIg> zrL{fL?V>dID}Pt!sGB4qFd(&iNH1_3_lDNtYfuIKoxW4qG}^{AfBg70sEzUIlpl36 zulT!h9YFiL)Tt}m4$HHsRxpZpf$BHssVCN8#`6mf;_RzQU|}P5x97K673vl8KGGB7 zMV^h$d`8KcNAlY-g}%j+YIT&jsbLDLix%lB(aamob(M$jzYH{oU2v^hG&>=`8}uUV z;-@e-pRw?So9V73CewC5>TwUx+(!R5_~Q?DEw|#8zgFbj080AxV1xac9D@cC;!rzK zXh_ITbNa~2uCw;u#WezZiw*c!AfRU?o5A2QJ?T`OQs%Vj=uB2;wLDw48tEdm!7y$| znQ-!(`dU$txynL5dqVz~Jd=f*wZYwSSrC4~OR+--iCrvjlrK^sJNk^A|HA=VGxPm6 zY`?RJ&-4cwO)b`Ust0qGmO;2VqAygT#y4#`WD27rR8%++nhjC!-xCDP0(JHw@`YN~ z0f8F}{&O1ThvrcPz1GQ3u5fHH0HWv`qqCtxrU(~aBVZHtn;%OoJwribA-XV$f0 zKXWz89RrK(u_DP$pQ7V?h;Zy6=1uC_eM}L}5J+dNLTu7AIN?;2-pc~}I->q>tfI`f zE7fd|g2DkTk5?EHg1wCgaF~B}MLiI!T{7-$=h~0fk4*NWxeyi9RiJKbRe;Z^j?{n*bW+R-T1CR=6adYf@u3l!D?j^Az^x zAORCzlVrAeH&CsZ2g=Un+vvsu;&{-oZ$6bmkfihl71_Ko>mjd-J}xywpeor34g`tU z@SFd zX25Y|TZB5QtLG*Sq%VX>qgp|!MY06Pu-BrFDq+0Csv~YZs;xvp+%FT>>7aq^fQ=XF z>WfmJc%a@vx0H9)TZ!V^7Hz=3rHYd>oPQNT7Vgigkqy z{O*J0tPM`*n4L6Ig5C9Qntx(JeK3@;YCq75u(?lSAI;ll*`jAkuv?bh0<*dzB+(zi zTjCpI|46j(){?n@Rb%at=O!}~-DH8f_triUhJyrTZF1EmU^?~(K6h?CG=XI}hcUY* zISN~KdLRh(aP=&lF~aSTjRI4xkZpDL!&{A(BNyQrxB}r`-*6|0n47NsggLhFgc;D2 zDR&p&>0vHC#Wj4Q{z`cedwB%ZDew_3va^T;>$vh<%%uYVB_R8@ebpHhaUC42+R;82 zp19+$+!OSN`}Ep!ED+>o!SN}noqT%bdcOnqa4RK^c8K8}l=)Qp7d5JbYJbI9wP>mJ zM6fz-DwFJ1s_Qk%1fzmIn3Lz^IK;fs-{IPZuUg(yx7UC)pZw;TtN@7a;X;3;$-mNW zs^G_}2P2P^g4%madw^?nL6(6|kxmsjaJV>aMdG|6S-7|c^aMaV0t3R(Np|X^nU!$R z4Kzo8dF7lno-@=6v|yd3>_*A2mx75C~V}nSMRp zj79SyQGOa84M!9BwF*S{VvJA(OKME;LeyVS`7g$G$!*WxpzDX1n+eE)3>Fdu(`y%M zW)qs&)^;4KA=1eRH6-!jFM506-yHu$mZ9C5)LOKh0D;203G(1LSYN852ps|&T)ZNs zoOkwSywRK>1lD;>Q5tR3b}P&0W?eSg_qfE3jLyDI1^mZ$LRklTNhArkG6zu+_OEv! z%%_2L4WulksCro6xs}Heup6W^!~r9(UUP&F*jz=$S?)dXG_?ga*vPSvS+}AWbnLp54|fzv8q#ep{!k@xVeioh6f$5-7<%;ZhltVv4k3Dqx9n6o zJon1x9qSoo-miF9m9DAA5Y|hRDV^wmTX>If>K)i< zJAB6ith@Qp6KbVCc**Ew_72|TQRf0sA`KO`wgM}gaeI#1ri+)Dp$o&^2Vma!R?HH! z1^;F;3ypP>`h;+qF;uy))hC^$TrhT0+EIui4Nyhm3INuf(f{Co?21XD z8R0BtZ4rIRxr^)7AqlRyO}@-?2l0M$i8y{KFl~Jqsp#6|L>5p`e(`3zg-nFDIr?eU z9mUk0;9=7NBMNEpZjP^HgJ6-RW&*!xPcD{M&QNboD{J120@^_BU<{Zfv#6CZ+-!Qz zQ8BF@y^FEPyY}nY%mLSLaE|eNZ@MgTX7ri~Y{>iI;z$^I?MAUERE05CSfkwb@i9aJ zC3N9BpBMm9-u77% z9uCS%SL9_i zE&J2RVeSA74}iR24V=!14>Th-%u&F@Q2wn3n_waQ1dH%eTRj=bP+Du+I4#pXPSb>w z8$6Tf;FhyP-2lm#Sg$XTN#>nrM(24AZ1YG*<~G`{LNo9`%}ICqPJWD)WVQ9c%>WC* z(^VAl^*|xx=nLlKh_-^CVG>(!9Ra@n;S+~bwcC4aA7vsvV33cs54(L*$4dCJ=*_MqBNkV{#b z%c4K#Ef(4+F{t@yBa)CP{%^9+-#`k_&I>ArffHkQFF5XASWn4EMo(eh8?@=ItRHEH zrrcjY{xc~jzyLtQ=qPe1?U5cXQ&Qe%U|IG4IM>JH1eAXo{KT)9fiTG)Os=Q+k>frkQ?v&Kv+O9?qNsJ8iY8RjWYR0kf2hIZPgz*BrvDD=Gx(Z4JQ17IUC)KfS zMn2##@qOZcsY8AFhh+K=sHY6ArGEPi<4>YEQO@;Dv|cx7@gJt4kQ^Xd)n=+<8z@-1 zaF;eG0ff3puGDHD{U=a0ycOv-JLs!mAnil%igvAIZ&1lCI~J-9Xi92Ca*|JZdk1R* z(t>d0YE0I0s1Pex9y(E4Tv^TA`_aAe&o8U8sRgxH-oiN?FiwG(Pruno?x$Hhl^fl%CN4o zZ*{O637@JLmr*e)lu;gY^A-a_U-gW=V-D`3AmP_IZ6 zsz3^BycW*5)MEKlZ?K`RFjl>vf|kzg$nZ#Wu=B;!2Wn0#ib*{E&IIm4paO8 zJq1hP;1KEK@bAU1yj%90&p#+~}Y*>e;=m*SNJau@nV zA$#Ry4y+aqZUq&If1V0#l2`Op|C~Jj2QRky7bW4@hs}De)y~f_o2`4UhjfGv5U>O! z_b&hZZX`X-eUKOh)OgA4eVz%Y?DfoXh!eT<*D`1;l;)K=hrgL?0vamy3C=?|8c1^- zK(`|_f#7m;gF$dhTS zV7Lmbo>c}As%3q&!bA~?tK>BtCpmST!p&`RuI%7SPqYIpAk07gzJ%709Nw`=E4f`f zNy_CX#;0ov#806$xqqPYE-!y~R1Qr1oJFCmEq01>24mhDzy)>46L4F8-M+>CKVV1d zm(S+_8u8Ho%{!*tWdO--!VD<4MKHu`3K5p4U`M>}FwXBHal|@9f@y_#`+9b)f@4TODL5o6M z${rw56%eCwnEtL4j!j~CtsHd@->gfTk}e=r^J$%Go|jg=&TAx2iK*?Dib<2I@K;gn zOQ>cpcs}p4xStZpNo)n_cA;&E+G0lO+)A>47f77L^8Ek8Cgzhw`7X=>Lu)SJ-EX9X zbEeUHwSSczKVfw&v{Z61g_B)VzcoB--XPSb=rmEhhGJ6BGX62UfI2Y~@8mwUZ6B@aaS_b5}&?E>GvphxJd$vl{oTZrc5 zX^@>c1kQ-yi9Z-(lKn zSw+d}_fL2?+T4n$sDF|0P)+skl~Mj4!2YZE_&4qMNh07WguKb!m$%HQDQXh5BBCeL zD5_sp#}PW9nsW=9P7j&a1lvF;ijsasik=ResJJKSdrXg-OuoY;y+ZR5xsu(LWnisp zC=(NhN+wwAfglM45zpCP%GW$gd9WRhlRxtot&7}yg4TV1slkf21SO0mQbx1&vEYIEGR8@Bp6*!H^EP61_Q&6Ac?V%%k zPw5auxY%~&*20&3!a{sBWK$3o0;qYD4Sdw zY02H`Fteb3_}x$-YTHIpqC1+~W)y2r=NXO~>(=&WDZDHWwh&xU=Qg!HPu7&bM7}}u zVR8fl0fGAtQS|8RuvL`^(rib+Oe8!I-1%zH2?f*ltz;ulkUz9n>*B@Xi ziLeHyKMq5w!01`5_ zf$KE2td<%)$|~dUVT^cp?$0T-mlD!od5-1)3eT$Z-2pdG9E;mjoT;3b2|_aj-(^|M z7kAGeup+i<);)H>PaXy8cOMRc6~nY-ILPlf2!i_GQ~+$C(~>UhM{xOCN`v?mIm_@t zCQa+13m+>PWJVHCj|iOw**qzQHz&D`QbF1U;hbP``W|gfy?cove&rf<->ge`I!LaO z$zvq7m^{vaqHrUaO__6uz=U|V)Qzx6(XoHr2OLie6hLp;yTPwOmq+6cBPt0CNNUOa zkBfL6&}Cm2-10j|yQe?*wNS(Y@zkriyXTzA%KUR{^FKjFFTMe)2Lwb*tV&j30@bsD zeKi`c%dOlINze_c0GN-V2NrpBGO;%C(nchoh%kb;-LMVh4{=Ww0sW7kU*e2BMr8k+ z7W}`J7GN1%pzXZ>)ibcg_4U^A6T6WA4A37P{~T`S8~26xyOtm5irLwN)AeF}H6^M* z&U%{uxH;Rpd7J*!Y1}hh-Jtp!^q$OV{(&C<3p9!Gu4xvSFYWEV$+){-$b7$PDN6qG zL<}40tbT9#YVxFntKur$SVdNFa`=?FZq-)zNj=ngHbBozg#&cqYzYXoOJ+?b-S{Hz zOa^&xwVtf@?l$go72QOX68(Iweesu8^m`BB$-jAY%|O>qmTe0r9+26cv#HXdUg6tr zuitW3d}W}uSgH%^T5wq~fWe~ckV)*8WNq{u{ujhIi>;`B#k2r~f)ek!ig$4D7ISA+ zK8)CzyJOKF+yvs99@+v$`&j(|b8oMGncTrevSW)_Zv%nQ<3+p(45qQyNT?49bjbxW zxUhVZkj$qN*978NHk{4i8D4!li+29AKS1AJICJ*d)!Q6}D!rOLDP7h|#Jf9ec(!Ty z0}@aIa9vBGMabY0*_DP`_G}{-fAN)&5JEp)956J}tzP><84xCPznkYC_CQJWVa+az zwv&+ZX-~QihTFGXqMVIv3N`3Qsvr5>?&xZ8QCNGG{9^p#E_9@#$ZY#*mK2$SE=Nko zHg>oN3}ZMwNpAMKKHOiju`rb69&;ef!kmqeLmlp3wGEjMfqdzfLF*vyBo1P&yxcoK zXXRgoze?51S6S#@IM@rE`B+MD!4Oi{tUcia<>u=|X^h5L?B{Q#;MKhKgCO#W8LMy5 zSYttyOw8>ip~bzoI=UNoBRH7W+9143=J{W@L+z9A@BC)jJ`x@@v)lN97x*bsS5u3~w##cyZ|*|fLQ2j&>-_OH zcMtXr)qm86%Qmu!3ZJ?SJ^~HTR#idKG8pv<1`$&#t81K+(iWYzb4p!e9ZzV}1sfy* zC=oB#3d8Kz@dn18MVE=SIeAG2=s7I&+|$EAgX}rT_eKQ4-gXgI< z_tMzQhajYs!JO)r<|!l}@PS&&h+rZB( zm2|p8L58)Xi|-;Z6_n;tlm~+W|1Ctgh@&7eLyeH|pHZvt40Wsg%es^$03|Q?|5p%O z!a!26vNMZI&1Ry)P zXj9Hc7`J?QYh;g#?^4DVm)YCZ>v2)nvX5syy)k#?;CJ+TJxi3cLR4k{;dyKZ zX{^(;sYW|M{1K?bQUVIm(~kc~DG7l}dDkCpUc`=)n7<)eZlW+Mo{T0u(FW0zgjmg$ zB`>qpy?_da2$~er5lIL2lhXC${VW&oJ@+|Dl)Df!rT$^9|9jdZkWtk=qK z`>}Oa4oLkajuyn6EOhTb=EyCgkJU~-g%S_1sw*7xX1`p{N_45;pS{t!^2LTY)ySd2 zbs@8ZH+L1*8+$Bs{6dubTij*#$tEki_Q_v(xGv?%UPT%@mF-D)ua5hWaJ~yEHl;+4>`W*)qntS^T=GD zJDN{|2&J^fQ2IVWizHtL9{EG6THCObv~L`M9^dEyEjAw}%kQ1_)Y9rzHiH3v>T^Ox z-5^PbsAW>w9oC8k@n;OZC}m`>6ezcsF@?}vBs8?q?9RACo}Oc+39?YN;Qb2t0bz>F zo~}%Tczmk}SjgwY;!onsTq-Hk>b#FrALU={;u)S((6Q(c z5M=kaMh8*t6J-|(^La_mkF%WCkve>Qmf3NVzJ%GqF7Dp8qflk1p^a-?0-FpPyXSyp z@nm|6tM(pe#fYz>F_wLHB-7HYH0pU&#f37RvTe1wd0?d(5XLpFooM|z7)0*P#X#g^Q8UqejPpeuTsTC_e|MX-=9Gta*<6We|C3bs_) zGhRsw6S&U*73svP`bu}AA(yNTDFP&Xy}GMb+KTS({H34!^;jzKbvEmLuJVpet<#|$ zBb^`r?fn^XXDIgTPfuaq(;DucyT*_?b1#}AnQ&A-UbkTE%HDjY)S z$V77>1XnElT@Ud)w0ozQ>fb(1061t|XD|D(Ocx#Ch*m@SI^ipV5wYZm&q z5%;pf3$2?i!{eXKFTqxoR9k{lOlH+*HL%G6 zR)gLS8vmPKPoBp9F)OoQ$QL9V6$?XDEv{Fc4Sz8Qcs+Xd>{yc&=*~XUFg>{0)n>5a zXj5oy=qIao-Qlb>(^WKKTROg(_dW7iqy3i|i7b5l)|CdPF`zS_3#TJ~Z#$_y-+PR> z+0`A}8$IUWRcl9ovu~eja^tF&l~s832*R6Nk5AR2sA_2OFsgmRfVoXr$%>}+Ls|-5 zT-zv%XJ;tsr~D%|wydmTd#0P0O4?hBub^(UsS~zcPeMOZSe3ru5!^N@^93cMXA^%_ z=NH+wlojTd4BDnSPrCDmPDA4wanwv76x!IPoAp&c+}JLaQKQfk73(Hy+8u{eexi zH`vRsi;pob=&<*w=smAqe@p9N`H-{LNTfqhIhgsmM#imk+vu}}*g{>o+5%5FIp;xs zD;@lIKz--RK=LQYWA0+~Mb#aE*}ne=*i&l5;^ZEqN= z8G;d}dW^(rP;r`YKhQDJADf?<62wynBK;tm6}n98Qa&S8Z4e%2#o68pwVSl)>9*T@2kTWLMeMzXkB_Sy=9 zpK<~mkvc(>UVsrrl|xf|n^G54mwzHi)kZs2nd8)*}Xp_F~2n6A{H;l*s$~F zi3ULg%~EP2ZIfUf89>+-tV1(Y@E$l-wv4LI!T`jWuQ^pu$%?XG6#Y;;5d2ePPJ(Ai8r+H^^*Et;(X_A+! z*@fDWe4&avl=X#IR-Jz+CY^1~D$`oY+mkHLkChaOE8wM(kbR_=Db^9lFlV1jWB^bK zDA?-NMbi{4)vu#@4z&nTO;UrOHs^8VP;Ku{W(P{V;}yEPkYmTfI1tYj))+E)W>_mp zqm*8gQ1Y6|2p+~}OE!*ET*%S8t9p%Se|PGipryR1uXZI6Rrz<`)Ra`c^dd+`l?jig z65qI(_ZQd3ASm^zYo%>;U8G}s6b126iYtv@<5+~1Dh;jK&gzzX4zo5nj#DzMb;mP(9IrZ zf$bAgv-{lukmNN{acWqmLu6ZZ+xsqUuL>dag)4%A((8#AGhyiTh0IV~@k)kG-Q3#~ zeY%qiWE7J0eg&W#7L?g{|M=VK~DnvbY zjW6lKHP-rmLz!HA5zxCuumUz{qZ1mmzZpYmd!&8n8e!Z!`G@FJSWXLkE4=h?~1}Ldv_!kC^R0%>Nqu*$!o4lCz5Y%sSwJwGP z#h=cIOd^fpv-#Jn-QE~Gj7=Is5>5(lDz;wZ@^xvk+jmyd3wcOT4zq$KQ)UI4Uos;xCe3& zzuoB)Ouizg6d9Jwe*1&53=xD>6+&9jbz>C=Lu){X(Aj*s5DP@!gmktkIR{_lO-q|9?Hddf&j4tN+V5@TzuR#Vw5+JK zEQ4LVbPOT7LqK0B7U(-ZK3qkyp7^8r5l?O%c`!ls8YToUS%(T&+AoZ#_W<9c`S&Jb zxR7#q0fP@cv5^5qfaSSfal69qmx_{a%Qfr78M`Oo)=8`1g`(g~4IU z;KSE8is}>zlY7qi$uHVKV)ojDUYx>zpJmD-aMzzpj-%)|wQ!v1LA11JH;ov}ZaC*5 z0h{m`Oe-ABigxIJLWU;JE~eroqWy?*=~tWyf#d_-(o4 zQxqDP;Z`lo_7cu4Z9+*xn4JFZ_#{}W<^=m=G~pK(@ckCX9E&;;nk%{&N)(o=69|D2 zvWeCO5Zf^x#bLXAcyK+)3a#&q!Z-FjLJ83;eVj%0QDr}DJ6bO4lhwm&T~uL~?WTqc#x$>%P`va`;Tcg_lf90dL|0bov}FgCJC^LXM#za9 z%{U5kVDDlJ#dz<#EeDS&RTUfZyK9=6W)4g6Fm zlj$oN_<-bT!egP*nQ+eTgr3{%&a3ch!xLd0houQdBnMCj_7CiIkL4%q%urxz5Sg@f zvv*14u;bm+F1(kC`X_@7aJu?BWBNn3k*Xfvjbhh7aXZB+#`TRt(93oo&8}UTKNw$j zhN;#w2x-RdSNK#;xu`WvzPP@CuTD|N=Ejnhv&V#xLJ}UMgUcTMYpaYv$+~txDz_+2 z+EN;w>O^K!V@;e)oVMU8&LiXjE=4~}Z+Uo8wbNRxnp}LozpqU(1-~Qd^P;Q#lq?si zb>%gjbOK>jND;ojwVNm}|R3KcG@mh{ZS3CFMWBi;F*q{j0MezqoXY}iZ!DRZwX5IS0#QRPJy zQHg=%h$v+!1|ettDK8v!{Lu3#i2*H=U18en0E}3yNjz4(S+nt8V%0q)Q6=UYsg;h`*$n*q=(P|OlQaTq%ez-?Cq#p;MRDqnz4Vn^ZvjZ3(HHYlyo!ej z?#SQT&EV!S=>_IBlYw*gfhFl!sJXt0qi5m?`qlloG?G=Sb2Y;G(B6%z4~E7iE@zrk z*oC1O_`jM8BZnq(oa3N371v=)->CdOo7J9o0ZAOBNo>CWJ4JPPk7J{Yt;9@mCJnNDEw4 z^&DynR`*Prhsn?>`n4bZ5ZfqY8#^{r^eMWTJU02qSp%zx%Q-V*hFcIRT$hIvVpheW z6EP@^6QoC$rhIboA=|Jg8)i1=FRk2qnpTVwI*$9%k;^dq*m#NOSmm6(eOm#!TG?99 zjGL*+qb@a~nuwkbfjErpujLv}jZGEVK#PD~i5N=<2?DGz&(*W_s{G+ewa z7dt1S;BMG?(OGaiBT5q^`9i3L&d7uGaTZNN6-k%lR;c(&nwzz-Fx@S7BDEA6jX%x~ zgiwGXXVju~G`Hj;yJiDj80M!;UZIBSw}wr8$u~QRHdTMnm9yiYnN-&Rl5nDHA{d5a zBy{M=?v}r1UKIb*1OYRem}YXNHW>;R3J5Ytfx=|u)U{2l#b4Y~Mfs*qpszB#pmSmz z`t`{fXi7AoeRejAHGk z1d|_t|)$0s9>Uq10$g9F5yO zRbX&4fF_Q`Iv&*&HOGoP%h%pge1Rlhj1s56##~FEEoktKdkmmW#87KyZae}{qlBuPr%K`s}s0AHQ&}U~+;3P2h*26Z9^MfZiy!`|GD_*%Z z)UhlG?P~k5;C^4B{%*;s7Y#&)=^D~=6Q9ygIYe@&=>F)E|BDnrO}N*gu0Ak&S`YJ$ z0s-&U5*fzZP49B(a$iO{0)JNIV`r-Adph88L+FeLz}n-Xb&`O*x|VgJ95C!a@d$#F z1qm;Ls&7G>DxePObO)GPGP$MH+ENI zbVL=~)kYG2*W8c^MfZD*XcNb)%C*Yli`vLBE3ImA4rfXpljG%#o2<7>BM$Fl_YUca z*pAegn&bfdVVG}~Xs`6#1yb2LX~6WbBN?Tx%ERQp&dvlFp2{XJ!NfRf)St;iIE|;C zW>pJuQ~Ty{BrR)1mFO8HqiadA+C`BlRF>2Z`L3#4Wo018f`Y6ehQO9;=n2jv>|OEQ z>sIx^7WCQukZ|ETk@FNRso}-zEBUv1Ni0g77D@&}66F{Fc*j`(qS_GgOpq96)DlVy zNU&_p4MT?SBLTLXxRgyMC+T9XP)0SFqfg7R*lEca^`g8W+>l%^(jyglP8%p9vPa0|#?_c1 zX?AO>95=-&P25Kf(3}QkxQ>-zl8Z@n@_phD5I)MvWjQi*DEs8PwJ|IO6Fp6=QduI) zF8LNpt9r%;H)yUr@WmIB1j)s^W} zNcn|#X7&7QW5HpIRL|fgA%bq?m!!km66=5(Kn?gM71oI~$Ac!ga|O~KbSNy49B@FI z8_^C0%6b_IA3z17_v3heL{4Yz?=TUBR#v=%1JEYP_+#cPDak2*z#ej^2-+`SHt&lhJYS++_5 zSI7Pwy;VIanN2qPBG$2$bOL$<4MQcDS<^oEv;(PL|CgU?j%3Js44?D{-|E1rc%B@) zVsR*K>_TL@#~@r4%NJWYn>h+GLe{KhBU8%h_Lau26$S>$ew>I1Wnp9{wYKSf^XXnt9zoMA+b4{88_>;LB}7A zd5Xz+$MrfRmZ`hn*h(-Z-cPJ(J*H<7!bS+>3cNyVcRDh%@k4jU^w^O z3tP5l-ldSzQ`6ovBrt_#xqtk9W#6hXM|T3M!%NI+<|x&!>)XzjG8Hssl@QY0Rz{-0 za*(i=s(cuY=z6S38O2`MtfNL%Q88@scyL*vql@~kFhk3+qngXT&&SBcrD$T2E_g|j zc2}~@Ykaj`yqq&|%cS?U@BXE7(1k2owye)r~DHIgr)6=Q{1p{g_d zG^w|e7pXlA6Sv6Sep@ z$)+Noy$x8wlA6%YOci$R>;ar`qro<6qu&2P+&hP7wq@JHv2EK*SU==PVW7@Ix z8Ja~8M}eVKymH^?9$r{fAbns)q_cHUh}=9?VWWbIhlsqLlUL2b-yUqqbF(U=_KP3^ z@>l_J8$}L80Ax}v+87TiiwC|~CHi+V%_a2uxfttZC_YnZE5t?W&cbv*^;OSUwl&3JbD@nTk(#`MIH32>vX|PWz#-8*)WfI+8tldon6DMmw+P_dl5t(h|bXb5p z36WXa_v!XY4#zv+fN~B#GCB8Kw9u8^A#;o;73e<5YVR4-Q}uR1sBvmwVDylSm&PCG za5d3IMuAB*>~K@|6tAz?6Ws zkf09&A#bMyQ{NPc1BEwZBg`8w#EP1vpP1k8j?W72mGVF!gh3uldLU8}=do5#!=vZX z+Oe~)YRf<2@JafkvQQ|ZGh^u9x0;baYRk%){)8;<^9A`H4(^&d&5~#fk&H#3)bFcx z>D&R>YcK)$g0H4n>7Ua>={xML&K7FB=IBv#3mp(H^IECt&!CXXF=gYiB?);ap;09_ znS`!Z3YZe1lq82oy~mi+J&#UWKjzEO9o(SV6{VyfQe0#;8deH^p?Zb^Bz-y8Y-8d_ z_~A`=p?|frS2nM-;M{v{j^&{H^z&2&UjwHTdTyvgSNz2xr@7~;ALAj+I-K;nI84~QS$Scm3bQ19 zIE!R`SY&|n{bj#u2(=L_snCt?hm&|3<#qu2H~94znlI(LCQ04*&aax60x+^yf@j=+?!3R=LTk-*ieyjt`q*t1iu!VcA6s`@th=v#GX?25niO!H5iJPQuq7JOcBOl_CBf2U`c`X zvK(83baupID@=#N1{!c$s8B%3rZyMr;loNni)@T<=pWZ1Ged9NF1ZC0Np0<1CkXGn z*HE7B7+0Jtbd8`R7X#=pz)-P}z6ArPoJcpr^=>9M8{8Y~#e`O{$0WY}5JEcqEYFqB zGnRVS(aeGoh{6vTCBn=Bx}-bLqrT##xP+txX7yhrfMk}mI~_(iZ&Z;Po0>NVpTkDS zysj_5ZsJa(Fmj^BR|8+~>131;)JeHoB-EzOf1h-PqREIlsY9$6)i8=_4)uJcc#_u)>35qo1NsA8RXomTvD~}8Q~Xps|rfIlvI|< zUa7lfv_c$op@^gTTxS)|Nn;B63#vH{tC4_nMOTO!3_PGZJOf?D351@8i)>aWq&8?1 z(ofM|7onJ$MmY^DLjJ%-1$w5t#!i9>$v_5(RyF2e2TFc?qax~J^h!r`j}*4^=tk~q zGy+yxF*MzQo{X_=(nrM`Nm;;7CX8R4t(RhHO>RAhSe>0CMem%sCDbcmy&hmJX*`?} zT%d0YOy|rk9mKwEOHwM}Ti?tX)_QBGq4ceZE9d!d;fpnbIHk8q*v|^2Birl=3@N;_ zZnPv8UGubIgBk^+Pcu-DK~>sokJO=HSv43h2!3q5Evg9noCSn>XixuNjUAD7D|w;a-xDRysZm zrmoF*qz9h2DkJ`giQ2S>w47r)T`EBK`Ur^%+RWhE zQi@GeLcy$-3U)jDOc>4uddHmn>BnFU`05nn{mM8~Y)di1^}`0K0bS3r5>PspUw@c3 zp}iWq7`j|U_VY%uUn27^mDVze8nMbWGwO z0zSQtZ;SYA?sR{I57CV6YiQeMGM*dlEVhtW)nT8FyDpJ2ZdWrU8Y!tnt%Ff}$z{<0 z{JxsGV#RL-`{T!1aovxBX=_TX!`;LhW~ZahZZTgG`O;`l3%}tZ)CQzkEUK&+aQigX z5fES2PfHcJ=2hYn`64EL zJgeVtF~8CT1)vQyE>+kuK01J@vcvPzODes%SC;|F2GBE2n_Fe&m+1vjpO@(wg4NKD z3a7#n0zu5?%eigYW8fl{)H)5xqX&hwV^H-5Kc#s!+o4H#Ib~WaVU7@2r3I)H#VLCk z6wPWW&F`%hMq}R1K>=^ATdI|PRLnW&w`g@4aH!%3!2%w`X#ahNrz)I5nEp_&*0hMRj_9cfyO!m`>- zuNXk87L@P6B;_ltTN9a+*o3B&OnWh>e$}L8g@MOEXn#L?TY!EWH5!Eoi35?|Ys(Vt z>{=r?fmbcmK}&;Pm4~c;4%Nj9gI?}LGCxN)<|bLz`N0#TCVWs(jX_4+ zxcHdi(sW9SL=T45IHYRip6F06PNJX!RfWTb)(-QXucHEjT?+ie1}sJ*L?@B!s?P^= z9ch}${uZ+#^+n-TR2w{}qQSwM;!|!zr@$FBMaSE20FS^2wAN2wb=7?1^MX(4)9j#{ z=weq*jbtBL~|xgf1aI7y;gbRinB?aEn*JVHy+XXEV&tyU*KnB<b%osOf|0hPHJl7Nx zp|A{V3<)J7$?KyKe^m~4T|WYxP|D}H*FzLx^b5cgqVGeb9_FdciXam_`OJJc_o&s4 zE|y*4J}boTRN`DQ^+Y*)I~Y_&V>!tk3fK>%V3Y8^#iRYR&fv#G8QTDAbqdN7)a3p+hVrQz zP!-dXV%`Cmni$jb{ydks&eqTK3ZbQ%Am9WB84xU-aZ~}Wv4HB^310_WA5_fGPZPxH zPkO5_ew}mQ7dMFOTVTYJJ3lTzcDLv1B^F;Mh<^~umr|HCcDvaQ#K;D*0t%h=K--4P z4?9Z{cn#cmEF^p#7I+mmpl;-YuPBNCKrEx;Gdrof5|hMso_AQdh67)JdW{yy9_$wm z3k=83Fn&p^$JyR5Gy!Ng_rax?bBrh)b>MwF5=MU~A)DsEO=Qis8xsAYJWqg{kgYZ? zFjOX`AySG49sRj3SY5;t3tg@(Fzz9BVEan`5fZ0U|0;T|ek6P|n2X>`wCoR?VPtTz zjfxmZhbmS#iqwlT88!8u-@BF=!Tlp)u}!s^d7E4-24euVk~#8^}M1ED!xnZ?{&xnKdUHOcp5Rds})Z`UNkf(HgtAcM#P zrjdZD0ppJs&6XuEpe`X+;bLY&fi|;wX}w|>V?QICgw4sVm;r3!eg+)eNffqz0*H*L zB6}eU6KGw3W6N2;X=2Tq0E8|+yfLZ+mb?W4`d$xj2EGx1=3~OAgIC{`YFFQ zfd=29d!YA}_dx)l(e~Ze2=Pkqu^aFb^u7YPy*qeYd*pgw1jsy?ZA#o%0{|DcJ&^AL z3W(1FfHyxro#)!OjaT{8;`@tuLfSX^myHL)72k88ly{Mv?hQZ_AP(U84txJ)$LysV zdcE-H<8&KWT}1%$7x$;`(~UjA5g_Dg_yzDncq6a_xB`5=wY}L`7g+F3dYcA_T*E#| z-1uJWtqY6*W&oJiX}5rkAM7tiA#Y2_L&RyQe(~ zpKTv59)I>u-PPlp1W*HbzN0=YJ^%oKwT&O!H^Z~1Q@o!`7K zp638!^Yoekcil_x*WScFwSdOgb5KP2#8o%lPgS{1Bv}O3grQxlr()c^D*}k#%}WtZ z?hQT!FSfO4hd1W0eD@oM)YJRZkDg~;{mKR71(y%2-eJWO84_C;O)toDvFx#Z8`k&# z@4ERkN{>&br+dDE*^>D=h>D6yq^ruFXAc|#%5{E%(knN&7vZ_J{~{3$i+DqwGwV7a z5RB*e0JTd&K|j=6d)svc1`g?>JX7vTNGJgR&M|!B_b%9()+$W7TgWUXT0jUrp1x#e zcJ;aS;tHi--25HV>|X8DeKkjHfTF9S*U3q=eGzZ z{j?~k|8G;j;NOqOybjI_iJ|57)cF3cF=4vHm zltV?z9cF0DnIcia4%}M`aD|zxF_cSeheCq<56{t`T@q215MiGbKjlPn;>DDgANh1aq9;pNM{7rIylGtGg~ygSB5-R{PIV}y zLg%SG(n}S^d&tYdW*=U-7cBmS@)rWt_O{l1HEC>nt|g&qF?e zyVTf8Z}!7N9`&;lR`^2u8xhsEz9W0N&bxq2cIG)e#IovtSULsKEwkcL1ag7J3Q1<> zyK{^%C}B^$WA9enQh1Z@!0KZjv`YMd=dx`IVHER2wr9+KH9QqNhR6N`DD$8Vg}y20 zS^pT^Abv}lf9g&%D$~EOin?N#3lEk8;)rs>p8nH>d87>(6davAH3%u7y3K+taDM9h z)L^xSrzJihVzTm;c>48qwUD?o=7-&%yR(HP6q)S~Z%}Ssixv8d7EGEqewdl>Xs!lU zh*>$>aHZ^$u*~$~A`dADvQlW#75QP_boH`tanPhauVRvGz4zz0gS{9%KYqyQl}nfWrU3D=w=h*3 zSXeCEWa)DWjDney0rrU`{1yk$ML%eGof{wHSjfiqno`<ud{datta=4EPzP^6upJ zu#PR$vkDhqV8R;pHwO))UNs+E4Kgv=DND8YaLF$rABwhZorl;exShvug3@bj;q`>qmU{geDa)vZ_Rpyd*dOmbgD}T$M|Z>6iLs zXS5G7I}iy1`cNboM+%=OLNYSl^=sVoM*~rM*sJz4aQG=A9TMxzE2XZQQr$sL6PQY0 z&`E#`zT?cVELmjU^bJfyc)`i47pbAqg882C`o<_h5yZ^59jRi91zd$>E@{*-Mg>FW z)acXUrR$H27X2mfX*TAEq&;J^`Q=ym3aF9lt7q-o13R-OnBPh#6-egnfBWoDOAqt3 z)pq9W>Fc%6Vyp*tTD2>C#^-_3G0D}7u@nk>EyEOENXVF=MYr{f->c-~X^xPIWk z*OkBe{2xn8j+s=TcE5&?e4MD`<&b}|n}m`UpAYXc5uPji@UU0oR3Q>p>`{IL!Gy1>+KEy;a?7Z_!@Cv&= zE4v$JwBMl`U?dg#ee|${vJ3`ZO20mApWy5xlxxgD>qW0l%FL}v*HOLr!2{$*978)I zO7Zw(n>PNd)aEcKFJgg~5C}8|mqs&dXpwZ7#jfUqQVnu-aJ4uiS1i#IKK5YiETES8 zMo*Z_<}FzI!A;vQ)dk^Qw}}K`+0adwdX}pngA(jncxwNCx3`HkY32?lNmq=n&MyLN z8SB*zVp+-VZ9w;UA&7^atA4WS>HAJ{AK6T>sS#BsZNeA)jVdfdZxq-GXhcBi++L}# zF=GDjQTQKs<8Og{y!i%*Vr1ANF5;;aPrX>kyS=2sL-8vWSsg!@SbFRZEoRwm|IkOdBQMXxyt47wb z76RHudi}ufj|)~WEAn3`=I8o>b;)F6!$(gUwemtZg@qw({w(P&*p|wkYP{&j00;{w zcc(I)Tke(08O?6}d2W48c1L`z$V9T5LGAeqRf+}o}wun{w@ z;bu=9dZIs~1MOw|7C=@~_$#=~mp&36D~?3jq-L>dV`zZLQtE6p7nt-LU+2u7WkHQk zk@MT1MWnh8Xju4aL@gYOGB}mcmdp21^JbD*BF(gWE+Ef10pf0t(xUI0rRk1eIL@~& z;o7yewX^07TNlc<(vs67Z$sXXn2M$d_FJJb}wAH{>*9%m`r+iF0z z_IOT+mFHOZ>kdqKkAW!%r$>MyC`E96m&}OJTrNxQfUWT`r7&!&63fI{2Oci`RoX~& z$qscT)pD5A(~24s$EA-`Iq)KVAF@pSVV~l}e!Qw#ruNwex>{I@BdPeDfp^}Ly%_^2 z(y$OyWi~gXsS%{5c+Vs&HiyqaiTP9Gf1O#Y+`xh-RuvGZMS;48u7ZLSj#!wL(Lo6B zA+dt-{8FQ3j0P9#25NuvK;p~Tl}Qi~Hn4zn9B>GabNlP97tSuiOlv;Oj|4O5=YY=s z)69KFTx7?#dN+L(*2=;Z33<%Zi>z@30^sgh#dG`;qRXG}o+`Y^J;sgBhllYxiRQ?U z#%=VK#TmK+Ft6<@>UQit#3mxG83~=Aib!q9*zc4$1UU1H6OlGP+qF2o>STcK=alvG zFGS_trt}qA3xZngbphX4y|i zrZ|BV^D#VYiMMn_!CJwr@4>AS2qVJ0Ah?62t)nsHiaJL=WmC7*F_uD>$LSW~SHmV# zoJ6f_dU^7w60y~RUB5aDx=53JJHFn^pB*M)>HGR26?r}o!~mgju&@{@x+Po(_vK=t z5@aY7F3<>VQ*lKW0`r^fz)@xi6^}x;jjcNr73Z(?#xQvIc(*hWF7c~m64O_+;EL-I z?3P2BW!EW_N8hKEBcpxrhqXFu+E;TJcTF?!L?R*0B~Y0qem>y(s{0qYJQXX225`H_ z%#&`~bcV$tuXklVo^^v*fIzF+$!$H$;2v>>>LA9~R>0glp;>pnT;pl7ZkiY1RlCj~ z3Rj&1=4EVgTF{H&dxmRik@T;|+`A|v5nYuAQ@ZO*VTurkAl^MbkU?2M2rdHqA7Q;P ztX%!yfY$F}8K$CuE%CaKDY19%-$9tS_9{V!$dTi0R&VYqP50pcwdB*|7yp`&|0`L< z9Zp;RzlOT8IBg;^(F)`@gF))P>^v%{o|^0fMvN=ZdF6G|8CVH*qsGWH^Uqg*Bb$Dj zY4D5BMGh8RQ;T-Uw?mWdyz_F)i-USh+5;U_{!zt{u+}ZR2->& z9^5t(Y)3ps*COP=*SM<4Gs)wv@hj@yfrgjZ6oYUuk(rb}9rOhOGKIBHC?}2Ui(%-R zUwu>4Y8t0HV-YGL^*vvq$ksxqnxBTf9ep`5$B6UFH*~ z1v;&VXR`8)Oe-ZQrVK#H2Y^XV$f%QNP!G9k7WDSc2TLJ0ou?jIVPAcP6qg+#TEW`( zPyr8b*sV}vRVTPM#AS=8{_NqOIEI404R7tn^WI9S91WSlBFj#qvwC?gF`ipVP4=97=NSlXtJ z)9(!mpYS>JQ2C$l=GW7eCr#uRKfw4AOdRHC8 z##G-|^5sPo)g9TdfAG`HHt1X7?oo}`!PCy#Q5z5!+1=l3_zCFSRpX$`_b$o;R2|QU zyFrp*sj63-cFpPtP(Flx*j#tpFk#tw4HK$CeI<%d9S`9l#9^ePE*383H|S#!4~Hg4 z@Sl3B+qAEl+?mVjEt=j(z`|?vJ-G;h9~Dd+kE}0LSN_$P|B=*O!YOm-lL0I9vKM$L z-er4&Q(5@2BHwfs2HzzXT3Ffe0Ujs~-&WQ^TAPVkmDlMdUr?b)^@;?Sb1#Be7G<0n z;a7ST<;4YUV`e`gCzcTm2fmng78kkaHv%%KRL3(=h~(u@*#u|Q`4s+$< zo1mifD>tl2?v%nWv45Ry3c_ezerk z3u*m+AeXd)eaMUFrw{ZLO^h(#gM#JuQSb7gFR1w^@M+&W3_|iod=r+ftc$Z_1I_W% zmm+&d)e1XY{qZMyvP~)>J!i;;>UQ;!^}pv0ZXUznn01!>(fao(Gr3 zRCrhDrQrrcof>~yd>~3#ZxM6?#Z`ok3seR7#M(`IFnWe!3202ylhQYzj%(vv z&R0mzPg4}?eQsYK!O}jamvztO3Ab;H8@I!b4`5)4#x*P0&^K3VZ27wj|0n+3IMEwN ziy8I9umE%Cg?$K9+?+1+> zyPaEMH87|TlT)i)Q7tcxMoICeM%1onV8!w5ZWl`?Eh1#ldl_cqT#)^H*FP-WSRk{5 zbJ|09krp)o=8&?ahXuhOl~l@>nufk*a_q}R`vYj!!1RTkRo<3SyTsLO>gA5GZ{^)0*QY|ITdI|mL;>KSPXsnR zn7pX$rX9X&Z_)rnw4=cda>;X=l?1ypNwMph(kI@a*m$XgEg(+o1tYYf&|7_r7kw^uJOqCm7g> zj^?ttP=H2HZYP&m8umxrPt~Z*Tnt@S)?!iUr5WmuCgwlWEsED$1us9AH904Qe&%8% zZOpq%Zb?^g$k#pM%TAqbk3%3E+y8W4_I#tFBbeWheg?tPzok56LMv^`3f1e!?sEE0 zf6L#f7BuZ7AwGmzb8}Y%6!NS)BjLBOxLdc3Sn5Jd4tMx}%k~hL{gUn3MV31IOXlb2 zKd51K`4K=p{UmKh_p=t^dbQpyHMu-m{t~R<|Bj|38;IQ(4ca%jy%u9q6QQkGSf9u{J`_{q z>D#Cjq#w6zs)IB1wM!8`Ytr}Ib~G9#41eTut$jD$10t5LYe-8^9yGBFV_B%O3fi+T za_D026I182+~wI~^wbsN9TR>L`U_iY7tIVmQlj|s4~Pf>8h-H7j@wp7<7rH256?$< zchhjd$M8J%U$E61utVA@v4R342s!d>Y4*<1Fj0AE^Wh2;Lu7k*mx5QQlt6x^}U!Bg;HFURl?b)We!@q2_ zxEK42#g9~g96bgi)`T$Zkz=?5A?VsvOoID>E6>sfyG0txCr78e6{MM7?)5Y7c{WZ)h-ujU$|PfNTt|Y` z@=oX3f9*TBw&m_Q7Zrn={hd+HeSuQ&=rL-(tiP1Tu|q6>iRn{PfL@j|bBOn#kGALN z$0wQi%K9No+BtV~s}hdrCWRj8_gfb@&mk?0m&J=@q??68Ow;nrh&7;wxCBZEV8R^U z*GKeCus5og&!9bpR<5!=4#2^z!(LHW@*8mDK;;Se>GP}Eo!@R|{8ARx|LC><0wC4{ zQnD=b1U~7~5wO}WLH1ybM_SA~!2K0qAhC(gIydu1V|*z6(kgPHHlKRjt{%j;%%6<; zs_WlgTxc`qTqWm1@kwkp2S~)gwlg_5`!@UD{MKVs-(vrAA>`N8cn;`5YK<*|>zs>?ue{NpPa>8Z zOx>mK+P||1J9)`H^=xKL4kWa2qpyOYE``A88&$z-5zhpVDnv*hmQ{_eqWFU(2UNJ5AQ_hVeDz9mh zTtEVmq-B&ts4`t!Q~wgn|8m2N=~m@ox0ZdCwDcY_*8~|bb0+f+j+jUn$#*EOs71i2 zqa5?CCv9)1?C^-912l?=`!Bxi={3;s?1__@5WgJX{d`eMgHK3Mc(7Q?WlKM znM|VT9?7GFQgSo zBF*0&?b-7kJXFx#TF3zB9<-IX04{Ab!bWc1fuMOpBs_lhu+I0zfZ>~Rlh*f{GRd*dg&{_+(BK}6m1P>x@6kC5T8SvpV1qlz5TixbZhtFh1syvnEqf1Bgo!zxh;)FS9dE^K7KEfE$+cuD>guFedf?c2YG-70yi z8~FX(a}_lNqV5i=5?&eK;OEtU&@I@V_!>AZh62F81s9FLHWowVai$?Z3Vl@I0I7tR ztF{>6eRzgcksDv#Z)ZuJQR&rQ;*+V7Kyk1M`FD)!FTmy*(n|d!%$wUq{Bvjh3_f5A z*98pX%vBvzoZ7go2YQqnK}l|R2o#MoOhNH(X=k^?Nsc&+KX!&eAE z`eg(~sX@zH57GAvfS^*0#(;%p$1VRY2Zma`VF+G>T_Gi3x#&Ekd51}eo*mO z>cNV^VR`nGObp5GU5A!;#QZ^3C0boH+WNK8Z!bA@j$U6(GaNby}-z z5&ugW+3iBXa;{f{oiB0z597>)-;_(9ib}snv%)rgw9I19(?I`TPXxO@f>4x3wJ9n? zQLxarHt+_jCESS5ss0XvGhtL2{RiFGA2LpB+2_A_&wsT+{-Or?-I~F!=a?scV{O)E z{nyS<@SUd|R`I6*=mmv%{SVvcSK{GsM(d(HPG2nHXGlDsqv=?uZU8ecAH=h$Tn=?7 zrGzt6y6gLa_tP2B6tvY)?kfvhz@$asfgWDIQCp&ajP~Nh$F9M`^sqlBm$IjP*YhG4 zrX~$ z2fR}^Ax_dQEiojd=?`c5_yB?Jvw79V9+!^0osg6Jm0LP9Bez5f%L+{4E?<<#TN;R&V< z*ta39-ml!%e@$0G&XnEi*plTV{0G)6@ zp-4D;|6gU874!>_Uz!GyvHu7AF!N(!82F&wVSQ37a!XalZf>9_mDf^RJtaMUhQqm( zEB&o|&LZU6K;-JR>JPEaMaH`ZA2P)T3d^;`Lx275G0xtLHwx;|5^O8z^%T1K(I9g; zI-|-g7p~QK)yp{GS~bma@U0z$Pky|g0H*ZHmw1Z^{BC=kjW5sdVcQC+d8>Bibad# zddkv{3@jUt~U`;i53&2jifJ1aS2`UJ_Dz}l@foP)MNt#SeO?+NMj^ta+56s5h z4P&#{Q^&cS=t)}iQBhxR#*slY209Z+^&pQ=mO8_2dvzdrZ46m2P2mzVl!mtOfz7CFjHZg zd-k6ebYI#NmmkcmfagHn@xs~f4t*GaIj)wEm+n|848b0G2mJL=J)wxz-pY2nsNoEs zO~IlS{{h5pq9>xEX~P_AAud%zo_>&~q9Z^*KLnj)>UDyS677NdoP@A41gy(JtPgr( zg$-=XGUJ9X$YV7SZ2aNL7cvnkJSab)XYeIz@;D-i)_MBM6ezEGAe#S)yH@{&W=YWffWT3iqgWLkxl+6^um zy|SDFVuD}@l!Wa6!O@GO1KoI*cH>2#psWA`Q;qNL8tu@S#$|w4;343Eg3{5pTnH5m zn_HEO=g3xOaI=)r<@e3Y>3!p3QAi7e&}OMA^F+-`4Qs)t zQa9xtKi{0Igvg~_VQTp(ocuQ%%4J_-h)?j8?JSouCSp1T`&FEv74%WV3(7AHaS#kN zJcNh0*B-Y$_J-wHDzz*$2=mZY&{_#UDsCP`MrM=v$>Wtgr%HxeMg<1`!()v*rZVfV zxye7n256~%6zZebAA^LVddHX4h9bvQ^A8Cu9-;GkXgml+NJB!uFf&h(5?%r`%HPqx zo0Egc(nIUM*wn}ppR(c?eu5Ojn>vGd{`g&YGZ%x+fVh`pdh?6YY^x~W4Yx6yCI=X< zUNGVH)=vU+z&p&3OkLWkfO_kRGory|-?Zg=<3kS&7`a90Jk`hW0<)SV8`$(*mLX#r zMR21nxYsEVTJW2suXJyy{nG0aZxvf^6}arfsS+C9w_h{-DJ)>chy2}pt^WiS{&Mx) zL_qHzk;W*%e2zby!sJ>Rh=NEv?8=N9{Ox0}P4r8}^?S*XqdbNvhIbvZ!km2l&l#?D zb8ZM&9bz&U#T)ndBTfCk!lAJ~yU|F&+e{jr*j@UB1^EYUZN4ExgF^T9K9it8poRwC ziju)mB0?U9Nn*(nAWkF2F=g@+<`WYcaJmVI3JZSw|FhZmmKFSjv9f(`ds@-eLpG%p z%kFJtugDoNjSZ*osAD+1@nTuw{!(&T!@*!U%(#?1Q)aYN=kLh}qA!JW-cDDhB^Bx) z+)Z2sOZ27FdXlyjIjMkt{{hL8HI`aHYX7dyv_)yyViw%nl0Qe5P45y!AaB~~7*z4W zK@2Hi^E^^%0S;_vMJOLBwD!aMbEbREWPZ?7Wo8ATAl8d-s|wA`-+QKgjsbSAA)Xx~Byd3by)6zEmw>S7 zjAoMEP;-S)czDJdQ>nkv>pgKt!$HI@q^As{;p-RZ^xAhi1Z3Xd+dyW6amFRjdJ5Ka zMWMAs;3+xCrS^46TQ=&OO+Xu=`UnKPgN1ibme?Kw&~sqGuM=r%o6Xm zn+5QDZ6M%+b3MQKqqJ7ZUh^k@`1YZgYqSBEV^3O?`371tUe5Lq-NK@AFt91+^P|mz z5#$;J;E7BC!N^l3v5+y9f9*Se4V9}18U;z3K|8hMW+cO7JxNTzDqfIu&{*qLp??9=Y%q*3ZjaE? z_Y?4bDSC(RHRO$yYiQ&j>>C>zIr0m$eGJ>LfkvbasVWue`qYNxzyLUA!J5BM6jb6z zH-DTK8LaD7-`0Au1=33SjvtUv3?Md1d-_nB^9?dD`w3Ve1GJir9@A3Vp{ySk9O4GbC z{q@=z3u^5qwK$^748D-Wo)5u5cm^0s8|V7Fm98o@&^uWueyG@z-ju_jJUiDX(8eA} zF$~b(aBq@cks1n);SbG{ru+qZN_xTYQ-{{0yH~gT6tIEWJfbLQ+CPGn@)?*Lvjq8f zRv|rT<@)p+g|Pmo1S?tG*w}V_XaU>_tScKT>-`Ipd^ciNg_Q*q`$U@%~R*F zd^J9Jbv1FJXS}&UWkP>{l?P>ndP<*^MU4)87J6JYgW;~aD#&hw^11Cx?Yv&#O-<#E zDY`QoK6Dt;=&e3mfnf2d=*$dEkBZLj9%mD|*_)q~H^kNHOX1?&K5Zno#d|e@lW*i% z9&#R4RrdPYj_w&xrRP`E3ZAT9E*xmM?fm0dXM94I;!71hb0uWJvSf(~y#NlcbFM_FwmqXoT;NTcz6<`wm=9o(%E|(7BvTDN)BM;^L8hx(ta`WPw)t zT?5?2^hIn75o4ikLf6g89b6E`x*wP&S*f8UW7M)` zL31b5ee=N?NHEq0n}GV?D_51k?X?mX(3FC}EMpuO`*l_gr%soT0yhtw3{4y~ODc%E zvjV;k_9eeSF{l`RA7u_pAP$Raw4K0I-8!4XNJ~4r*WUbK(5SsX{oyiA4(ySl^TJyq zN-&N9T00FcW^e?4uS%ZZI9BNRUWf;6T1i+ zJj~6MUehhaG<9??jT@ZpX!21fw__C*(h>`W&2~AN_`pR3pN6r5{TTDiv(rV)@4~} z*elk_1|%@8a{ZkSQ_T0Z%+KAhO!Zg@c+Rdjw2&WqC=N9Fc)f7)fu1hRN=Z-@Ei^ZH zWiCc__6St2#DQ_?xBGAQ_L5JM04odfQW4B&z_Gc=oJv1mM}BE3!EBi$cwvO*K!!%% z4sjlD6Gk&DQ^HOxQdF&A6nYMV05P&Pwt)AQ)p4)qV@~t(=6KmMMc9ttA~Ede5{fI~ z#22qcHRPT3buWC5+Kwkqm(U1i<|$HDOR6IF{n5Yx?W$0y1ufmc(+S84EL}#%;Ya?D z4$+!?Q8UK{0tsI7C!)2%uL$9u^sDOIagbmPx)^ZIJsG98F2cQ|NL zOxMn&S&yJo)XYX=rD$JX*U>cL_4YW_uGN zOxinXUJa54pILnhm9r?pydp{M2n!Lp-01Hp_`#5%CHM0w8HJqCYEq|sB0x@N^d=VgZL<#t4HCKpGf-^a@$!-P1mHw)#%S8mglG;T`_-&dIDEAhWM_)AgOPu5rXJr1D~vn7&;#m}bvVRQ%zJ1Vw_u z7Ck|yw$vptoKilXrf%`-$9AQAi1US_s z$~_c<57Fbo4P8fAF0%#Em64v}Gl=td%{FBOW_|e5H~iWrq6**IlfL^nb>pEC8Usf> zVVTNyqX>jWtyisc%YTc_r2f&UQ;aSJRCDN;wkp4Plle{U29>4zT;q1dNP_djiYEfB z)ICoRA)aldix*)8@Y6uA8N-`ghn+6v;Sz}7Hujor0kphoxJZ*OT|AeX=)eQcTmrB& zk*|fO-laP*A@v|DeWpU*veJU3|CbY2c#aX5ovJf_yH95$@zO2Q>L1ag=34zK$?@GH zZxdKPIM5>&?0z^1^ueSetu(-fE#c3cA=$-SRPGS$QJc5YiAR3sbNz z<`!+)mCBva?(4dOAyL8!XiJXTeRwlJG?0$fH$Es`iHf1KIDH&t z-AAU(MjAU5GQ_tA`!;Ey(||AG*>ZBlM>n7DgPzD2SfW3lzpcCovX!Df@N!s55RtVv zk2^O{%8Rp@EEKb4HX7=*!XRd-cHWxC{X=Q$o^xRS+NWA(rEZbO+wKNVg~Je5|_+&x4i1{5vOpoimj zo1JtiXQA4=M1L?++R))PB+y$lc!ljg*QKNGB2JA<~WVJl^DZ3s_*`bQ?_XCm#d%gnC-FHl z63lJ-m|py?}Gf-WtJz+^&gvJdUp-1*_^w z4`OuqQq|+)e7D9wU-htTp?@@wuN@7?%4YjoYz+#j8?~xpS6Cz>aZFpZqM^l-3kLcB z0dhc%zt}V*lGPAghY_dmCJ5#U`+@RfIGyL8baeCn$r;MABuEfN%ankRPg!FT)amUK3@~jW`n*jR0YSNWM6PvJh&z4Y~PVl zw>$r-=tI0Bzf8i8(E$iFERuU-=EVh`#U2!Ky8m=lln3`q+nZ9yQeJnsAZ_P6 zhP*PkTSID{ z$Jhs97vP~Fu9^UDzve?U8M?YXT z*o1t-zRu3<6~0pcleC^Vac-jR+S583okEhR6CIz&uYj?Pv4E-GO{SLyoj5$*+dGX<=m090;3vA@jb_q^ z0gd$CCv?rub#Xc&cce&?BaGv+-^cLXf}hYJ-N~6Ngdvine>vX;{9ZEv{Bb|-HZhs1 zy8>M_PuE&GysHI?(zJ8{*G3Z_l6dldrUraAWI36D(h#w_b3}Me0Sc@dGDt8ZgaII+oi&)n zzXb~SD(q4<3-^nQ9`4;;k8h;n#1447hyI5PhJEpI+xylibatd``jW5N&cF}KVo{*2 z7v;^)R_rOn&Jm0iUvp5z_xI;vbmBV&M-WmFagAp?Rl|xfpI}wh3@Ck!+Jq->QZ$l@ zs)hzIg3*Ip@HEW~2JFAVnokD7NPjySz$VHngqoUG)4eyl4TcMgVYtQeWn3A<2U696 z(J_^l0aKwl5X$7veb(5cSB)wUHuRkcUe`<2zih9i!iy-@I$6x-lYPI+aC3E)PIUOb|~q z^IxO{wk<{(JY4qB6vN7p@h@Az?-=}qA#hXM_1Ru^?87!pR7O&bUZ6vRI_{y_7!*1L zJ2akVM_cSLz6o=B zL#6ZMRKmCmZNkELz*IA2Lzx%L2%P{fdn$zAMd_S?i!if)7I!YcH)$T zxcUKWd-fkBlgtWt6EL&x{Tz;O(s3sIzFr?j-NrWQxsW;IIioA$Sbzy7zwn4YZxo`7^@wN9?1%I^*N0ReEmtf2x^lw49tPmnhTJcPcDgPv!`GA0@Du)! zcA%C`%@vbL%ze%MUlp4tioYGN+(dhcy1cxpWzC2e3BV4yjyn{#GK)DKmc9t_msDm9 zqR+K=P6P|$p$Vfvc#BldcSo+@_2la$<=>}HA1d^@p6g?@^N4iP$NAkM)^BS*GyKB_-xNK$CTL?6_Jz@*f%; zK`d8r%{6Io@FP^`_w|YPic3)-tOC1BpAMRlYH``*i^F6Eo?;T{_8%P@xDg~pSnj!}C)Am4KB=GocT{yfrrXB>r zfGStHHcsPfNfQJrXfB37;_ap7#D*@Dm_-D-{&-METvYta4`SvRr?Y&$Ld~%#uMfqB z8}R9T2;b;RE5#SZiDht}Dlt6CjNE4;xyHXcuO==vuU{-eIQBHDt)CK&^>yL6nS2hM z!^zR419!5%4I+6+0000fMIvaHOk*V~L6R{>kz%KORuc1t#d8-_O65u= zw=IE&52>!}6Yjp*h8q`ERoHqV4AVugJ!l0<`^xahv@bYo`BCqIMF9|($`U1#;t2xm zO~P{kRGT`(6OfDdND_y|K&xHqc72e5SLkzYz=dYqcj9>vR!O0f++Fc9g^tnZ=h*Tq zm+vTWir1a*wG}`ke_Al)0K$MEi_@g@oMiAGxL}S^AB6YQh5yLE9&C?atU#o2&Y!uo%zXe#v-||6*@_1NgjrU8 z;2at~X!-JIMmbdTPYmK4i5p(E;!(^ldI!kcV>zu){WqX=wgujSz}~<3+iloch1nhk zbHpBv2QueN@8QC{*mm)C*aA*rLk72TZLOQL#RN9GV%q?QkV$b9jSu6`{s#CUhIy+= zykpsDZbH9q07Zp@gK4d4bpv$F&;3fkIhfTc3rGwg6L#D>7sR+k&8$qc(?tiBCF%iu z0WA$3YEP)P0i;L-p;!x(!aCZ86bc?0(|4f?58K^#QyogXoKZW% z`+y`26LX(|A{E8uL2l>BB1I!-gFmedr)kSEb;(FD({9y(p0mYx=rZcq;l*Muc8F3{?9KU(zKfSX;8D2grr{b@Vv9TEYIESzPwi6y6s35oAD#szBAT6X zKK7Z8DXlCT8)?^GWYeEQ`&>Tj{GB|Sa!V6y@>3Q;NclNFr`Shi6g1?NX)256p~yoP zYH9l=wzDhXt&rWKmF!ZVK8Hfl%olAQ7aH*iU^SxIX)^Q5bz<3NByglrVVUsNNY{xH zixzH1Z;ZXFUd)64s)lCnK03PaK($ASGZyp5p5c;4_Mr82?uR^IPU_P2#y*qHA>GC6S z3GaB?Fi~F4t?6;STaCB2(BC1e3)NBu3_x1JjMj~2v?-Rm%LOI0dDpVnQ;ugH{gAgN z)AV#{VD~&(|6)!VHNFfYt=sQMshDCy9U`GILT_rhXwoy@qlr&l+*P8)nz|_I* z^KnlD>8d&4Il8)wHepM-v0s4jwp~zo#(5AfdoDzMrP``! zW<}oxjM6oQu?sMry=(U-Y*_!f+3f)XH)S$X_S1Y8|5J)LX_x-^-IQ~#nwM;=n34^9 zoX&00KVB&iHXqKXy+*)swU&CyIA4# zTy0U3@5n|XOZGOUYWC5YUwV05ZDQ?B@%>@+=N}bP3UpF05QWm~L1-V`=|QPI?mDY` zoL%7C0Y1~RP+!tHeGNn4&jPaf+tYl2ASP8OawSHmh&QSWzfn11^ji31jaMVq>$#96 zjIg4)TT6!WH=DFiXv>cG*%7})pEuWR2RvGe)qrh#L$-)u&mlj;P)JpXf6*W`v5o0t zShkS^4(mU({`AYRYR+R1H zb^3FFF+3Q+xJh))Bo>_C+gJG286Vo+^I*z#gw*2klWoi6FQ-oVuIs%XkdAskE*sSS zFiFh7MH^O#`WP0Z0w2Xf^=BN!T4*p|<}D`sOW#C_GRX_%Z{&Kqf<@ zcseNWuMvMTC9!2W=Q$z%8gvFN5rf55F57P6C&GNfWmbdPq|tk5Vbus|chu&d$FMV~ zJoDhyo5QlvwzD(9h*}-Pmj_7CNs6yAD=wxsBV8tv8O=w2Hs#;^_8v+^rtGx28v2F) z!xX~u587X5_utp=eodCCw@>enFl~K+DI+&ZmbI^q*PY%3SFO}(5*vP#B`dSDQZKO& zZk}YHyZU^9iY)_Cpzq_;os-X2RV{YmLot~h=ykjzuP`y5g>SYEc*R@`*;?I;)oS#4 zfE82g&Z1@IyKx(LMhf1x_6x)v=tF+W6|5Y*3RUrh=E)yf#9jusEE)O|@HuqP05-HQ zD$!<(Rx{k1kI7Mgn^JKgqGn%k=Ln++LF7EUc-!Ktn7f98U;oh4b>qKLJ-K|mT$4D0 z55AEgxdrAvt7$fNu*FPfQcL0(x zk{j))m#97R;1ZG*-V*U@nE`Y9=?@0q{8DO2WZpZYv_&uG6D94y19B{h^0}0WN~~^9 z2+bE@DxXAXdB-SDd2A52*o+MmEsgjo8Xl&R41Et|om6|Gk4DFWyzw__KF#!SwTcUQlTamW-Dq5DxG>D>pHet2Py+Pol17&-- zoq*dSL<=6&V{mn{uej7V^i^m%ezSXJ#Tb(!OEwiYq}aTbh&EdeHS5 znLcY_#(6hWCh#C@_mbA~j)g2MHCgyfsp?TR&@lWEG|W$n5)4*C34*$!Y!VHbF-<|G zAZhP%&rMBYke=x3B;>eXR;^Xz`1uY8BUWnw>SqDh#41Y&Lehgk+br!u8i170C8qIsO4j&OgONV+SMH0Kw^4R1`f3SB; zx?4HpoR1ArS%HbyunfgY&~s}GxRvUF%$eFTz5>efG*HxpH!O1KP=9OL1G(M#ue`Sl z;nBlN$k?UXIisJPbM}yOd{?3|*ERb8TIUSWLy!vY${ROwv*=Z2M4FYWZGDE$INA9{ zt|G3aDN(3}V*e@gz%Ek3#^16|#Keo|KJv;{BcYzbr&fe6B>v(Jb(#UW&(p8!2CdJx zGa`_luFD_Y*?59xodKmAX&Wu&V7AJbW^f@N?v@!|>ubP0HJl%KC<)II{RN@#6_&;beetAppM5Pk#xY?>Y{CkBb`}ts_y7O^U4rm&^9lR( z^%l5zr@i!-)wHD>Mk4FUR>qp`<{AhzHSPrG(*jJsW{4oSZfzcPP`+&O#)1yPkY)hD zGW#A04KAkotuIq{TSsE391kL^^NahvGDh}dZ+~4 zrfNd@sOqzo&f1#4z-K(@iW0<=&Rl**61LT6agMjzp#z(X`M*UN?+oeezZBfj_5 z{w@2DX>ee|9hG7RCbi%OircdSY^%I1Ss!lBxm)M0QCY&eIw#|^oVy@IPmo<2i}ZYt z!Opb#7)M6dLnIk<4r3$8C07EqN#hWNipi;ZjuP!x*a6fliNzZpLvL%*O!{Yp`Oc)2 zjx5{pXyA^Us(Puo&lbkaufuZQ3{nz!#<&^X=?n#)S5$Rqe5}qrMHA^|Xey{LzvReXR z;_SQEIgJIVk2#Zc=X3MuJ$@Y8C1{d4q<`otQ!r;Q{aLDj#vku#5jo<*DW2kq8vzIx zX%++MWRfHCFoBQmT)+slw-kwnhgyCjf)RoNT`GI}{3bKRaafSv6Vm=@yBWhFs`BOj z2j8o_dtjXm%M2nA*8=^$Xv219nIyAE>L&k=q1_XlL$r8t;9aO7TDrpQF9KaQ(WbdBw@a?Yeotv8RaTxc?FpSs{Fox_;c>LCG=LE=R@B97oFDnm`&># zxBWM0glkkpMv&VkgW8OG03pTmw{Xz{h$85~#y~^X%4N++D70-+E9vfD^F|!D+uL;J zjUW?shEV61*kO1wsS`2##-ISe?k*p`=v*G{Qt}Df1_kpYP?&S-5FZBGY@p!+jpo5a zrm9fHgRc$IyIaN3mOG~uklaD5%rlSEKE8<`LbV%U`Ch_pom z&9YkwV1?8I%KpL$Wgb*>1|n5mDjSU30LMOK_)(I0t8pv=U*uU=D>rQ;paOr2Q#avC ziofY!wn3V%9r$YUuxO7RE169HA%oUqP7}rR{{wxqIUsWE;i0dCj@EWJ_3BXS%&{ud z?5KoR*z1`&sou3BfNzTX2>>|mrsLTJDSk}wC&~A~b*jf9TtCS{FJN4~s{<5af1M@S z9i7PR{C?us*(V)v-bdyO5_^EFp3CZShVuK=Ny-c@bZo)ykb^R$Z{z>~002k4ye+6{ZLFrI0h-{sn;ZrW zNnh6Om6S0yZjJaXrvjKx6LpOPjmma?1QD*qSIP;qypHLhSfil>I|s^)P+bgBJ)UZG zH&NJ=le{dz(F<-%O0i>c*{qP^)RO|s`JEeA$C8UjyrU~#e$pDpi0(pt|H4t_531{y~i z$!n-+j2|3k<-T$`4QsBdf(6LbFn935ySKKPTjf}g$7#xA)T%7W>Yy-^@C2;bC11&c ziV!i}2u^)2WB~MuKtK;dY?sRq)p74WnuH{d{kB~?gSzfJb(@VAQEn&W@~!y8Edoui{3!wWYC4~M3@mwXr63#~ zXi$6v`SLA1`T;PH0IA5kr(ygRK;Qr;f{UCbaKLOyg@Wf9Fp=(m#{ zk4+bn;TBH#O%>3cf)4WB^#(#7o*y(a9%P0V!G+#rB}cA=rGWKipaR4`Svz`cs6iV~oRU>7>=(Kh0 zcGN1#a${8}p{;yR3#GRbY{xRi;^Y~EMJ7`>?8rROU#Jr-6hscq#kzcC_Chi3HRu_^ z24Sp1P%cS4u_`^lNZ$d>m{1iZ4in&+`NJN-g%1SED{Ly@HLv z{|(Z;8UL1DQTUlm0-ZKN0V=U$f`yQa0;LCQcbBBANADzM%#;bC@ghAd&Rt8}qM!`Y z8A(Yo-swS^lGsP_qwmtxis#VD8ZE;Eys|dw##Cmkqjs`--xJdI8tc-OYyBzC(Yg}J zl%T8~Q7o+1F>dTSh$89FF5fWgqOUsOfj&c>g>i_E!gL3hN{pzQG?~(!Jos!&mu6$ff=@zRT6*0nodspTTfH8**=9{TAUkYq^UoP_y~% zo{MWQT7tQqz78%l@=pe6*OM*}JSI@aZypmUm&|^$U}Se-3&U?Qa*9r>ouxh2WP0n8 z5Iv*PQ}4Ugz5(5&1x7r*R%A6zy|+=-Os9YhA~8B}HRa*068nCLa3XOcVCcDlIM+DJ zq{VzA)s~1Ly%icb2+PD*U`%k5@}1QLNnI@sbR&)&MHyJX>9;xcf(If9hWgAL%3sMz z)b2BwzN94hY6m*HTR`V)k@n2L*=8I^%TgP=Moxbs8LZi==S^sRXXY!EVMi=UaooYP zs_5B`iwt6}VD~RDa_=6_KD%FNbOt>UGPj=5P{$bgt@d5*Rtek8z8HyWU>b?^s-*lB z+(9?y0B!9lZI857b1$?~S~#5gs&SH4CCNUbR-o)y-?Pg7s)0_78T0dziDN>YE;r6( zSLwl$d?o4f6l8%{m=RShKooi4B8ps?nBd+#vYoRnB(9L|sjUof_i6REC8voYl~}p} zXANOsvuEU(T}!mpv8R2#pHZ9KKr=-Gr`3rVTpC+8Rb7tzmdcU@YphbS64{MEYG8$U zA%Ak3l{i|;{GS$5_AtX#)5zd$NYt>iDo<`BAr)MAsIBK7Z|>peTPgHh#AybzCx8%+ z<_M?>r@Q#GoI=7}_;i|MJI#^i1>GlFg$t7u2yO=*hkBRi=oC>+(T7o-K%#X1|RB1*pzAP@?I~DlP`-+c(MdNO^!9>QG>L}D>&DFn%MUQDCDmy?Va`e zgS#lU6|Yxiq|c+h)66LN!e+rQs%9Oe2D$;^E4O|9Ig=&rry-eRQt=$;=@2_40zO9& zU;A9z{g?aTa!1fv_=*8gGXh}sGeBt%VUQL$d)|ujMAYCXa2P`Z&QUz+B3uDbF+UT2 zJTpZ(2^L#bdh&lWQWE`PDb8N?F3xa`Mr{ub@!r&(^SLg|G-^!sKZnni;+Es^o7KNG zK66LuPj>ua{aO1jrV-{r%kU%B%Cb6d`9I?Qd_3fsjz>587KH+o4eL;SKvuWl!-Yd; zUl8IX>I|ESe`y;b7&nEc98Qhai}(m*5=*()gDkXN&ug;3*ZN5zE9xgyA|munG#JpX z2-oKK<*g=WnMw}%wn0n!cX4N&vPO4di5~TDl2kl|O!|#Z{sjrMnQH z$#Q9U4vyjrcAL79sAgFgv=a%^_PPx$=~PV&NZ!X+Pqn#m@r*cODVg+WeOSvIaFCw*NOKxbvAlw*LP zK#xKMZ3(1CG0D9V9bMQurOL~yM}q`jE6oq*z?!Wnt1VWIv zBghQ{3cVi@&53}|wGE~Ge-pX-S0)qpH6Ea@inB+2ZjZw6v|H+`(zB)HrNdY$*r5W?Wh2t z++f!5Bux=yy-11>1anxJURReOo6oH6GD$1OfT}(dM+29hV8NtyjsHwyoLV>34!IoI z+o5JP059)~wER@?`nbc~FujT5ltp*;RoXoKUGBML6N@bbI%o9KUrt^rm_;eZ> z!@eJo%HuO7G;wKs#iq7u#Cb;Im6NqWiCEipQ(pgu0()O62&x0%o6~c#KgezhX{QWM zfra7&F0Un+sgL7N%Hdr+My8*QU++%8Z75g=9cth?|%LosZ{o5_9bO})k}t`5N)6YQEiGF81BQ=>0DFK^q~`# zdOslgV#vQ{Eu!Ql?Iw_vH+~z^mAH-Ag&fP#QzWw4f&z{?GJ#9pktGb*Xq#i*7@RoG z{dwLiqJnjY(0#E#yI84Vshgb?`= z&o?F6$4Fp3wn&`p7?>OZj#Ie8CFH@iPGndovuYrBc2ssUWN<^qJAlg@dPvy+9CWD zR;V@vO2Xk89pqNSX3}7rL?Qz#!6uY_e?L8+HuM>{q!S~0ylpo<=h=gKLA2%tYKPNg zU>CFE;diHrPNceUZa7NVky)$O`>E6PUuTxhUQWYae?_07D9(`kGSstQhA7IwTf38` zJyCk$XvhNAjk5@>IMpcNFs2S}fAR4n;s#nK8qkt=Ln+7>_AoLid!s5p0q;1PT(_dEiF798YKYk=FUtw|LbKu{7V+tmdv|RIfAs#mx znl7YvZvaFgiIWay(ua^^8Z@fcV~N0>8u*?-9NC3@0$hwY!#2*p=!Ct2jrj1wSJZ|> zA*4rkn#jTOEb!uF{R4Hh3A2C!xgV`zefW9#BRlP5MtQ$pkL3`R?(<|cC|vK&;Di8W z(uEw9h{i6qS(H`d<9L^xv%_pz7?a9xnYv_Cdl1Qw*xbiRG>vt?G;KB9kB!{LHA=!_ z(u7(u&L{;IJ@u6@8x36Lh}jw-)7OmTNRRuSV?Y&0%x0o_>jV`tK}T>t5yPI$U7Y8l zFtR_Y(J02F`Y7ksGoZ` zr#{TbSs-rYSxilzDGkfEu}8_La2{wZHbo~uFhpXd;R)~<@l&ILs0Q7-tZN7g&&~y| z@pt7j2r0H_8gTVZ>PQCi?|_%9df&X61f6k!%B|nS(3?45XPiLOY+J0@;NVs=Gc(P- z?8v9=PLV1 z-$XqPVw`q2thJsuk35y=c_?27M?WwMFER~X0JViQdY~ykR_~|#6S2B-R0naxe^6v5 z_i_c7+EQ#$;QodKMfmpYOlyNwtwhrWRn?ua7hX00XkHyWF&mC))GPwm=^1e z#mEYKK@E!LL+rQI3a|g&L46!s?SDUJ|6}YI2u*Gt&*Q8k@wt$bH7+n>2IW;*L^245 zlJAoZu#e7g#1RGCZA3yf6;#PQVO|Ht;b)ArBJBPIP$na_AZ$kFQDW$` z3FVdOMq%mof6|`?pZoPksMw*(5(LWCegj+T51G&EGr7A4$VVoE%~$jTM~k+J8<#AJ zPV+*JfEuggT;#gqO1A9f+6O3V=hXzX{|8ljT0W5}0@|Js2EJI9Z~ZkR(6_#?S+&}V zJzzNw;E~1q3bo(Xn&=X!kS5qnjT|R+GxYc4t!lqZn?~m&N(1}>0O%VZ)T!! zGBEGcr6Vm4MUT5Gp*u;1rwJ0+afE`}3(i6ho%$M>;WjK|5;`5b(&L@py&r61oIWQ+ z1|^`P+@~YtzX%3w{lGX@_CJ7hPD5v4-Kc-(;`C@NIc>_IXD>6GWx(;8}2Aba|JqYGW(Wlb>0ny&)%K4O~n@-FnF{{65$dC9v2m!D*FQn||-%q+7AjIe~Ej z1+uO`Ka0BZf8(L&EoQM?qA-1g(Akg^yaks_$kOZ@0E2*3f&W0Bz^NWSf zf-j?(rmw9(2Znz7T(dzsRm*32V10$tPG?;&$J7=NWfJMCM;}xX{Ff4DHtC`9K^}le zaY)V#^pda`DKEbjGE?J~A_XrrNvS_wQyLR~j-#mKvAM(3wb6H{D?nTk;!5^WR%^cC z5`Wafm6A=ip*)CvoC31B(7p+)1+?=9+lhd^g}r%_($FwS)Ev$-bN(8yb<)cwBg{x8 z*AR!rq6WBj=uqE`D#iV_oQy_<)Rkm{lJEyZ8{u1x4& zr1dM8tQ1+$yTE<-+zt+AU;Fo|F3$HhZQbY##Tcc&U>xkHr~6vyUkso5w~ zzx+;VF|s0rJ`bBny-bp0%Y#5nH7#2ewcibE7=xH+P?`b-NdAxxK3j7)-%dZdfL{Ph zK5|ai^^dgvywJzM&Y~DiqBZ(eUb&Ngm%38?_jM6m@gJ}Qqf%|lh@3W9lBM1EE2?Fp zU53Y!qqyOflb@TZg@ilBFQW882>LJ%*Kk6-@bLhdr|C}?rhpn$Z3x<0GCGKKsLCNt zd}?SQ4km%+mI|za+4ebnGed|53ZHp;ki+U0h)Y9!H@T^Saz0e?7<$upK84E1zR>AD z4_0`7#XLxs`$}k}s+rUyQe(qAKH2)1SJ8dh)8blw`eGX2Ts>6S#F+jbpt75!mkurc zv1~6wKb&$PME-4eC!Jc0N3*T}n#F6cm3)#DX~4HD`$xU8|JwOPS*%-K!BvUXiSa8i zyLXwwE|gD`Mts)7*l432uKFMCS7#U#i)@~r;S+s8C>1D@pAWEykPr?n#?$eQ=nUt_ zBZcbJzmf;?H| z%e^XLqH%e4Lq71;VQ}9wYZBsp-Vxm{SS5*X zb#X7bmT*ApPEa{0|C!5|F;M(n^MFjS5+rxOs^qkt0PTGaM$`crS5?RD<(~)BA-ovE z)Bqios-j6eJIND+&IrFfqPQ!?bEx96(RB90E0 zSr`>tibZcrp+f;vJhsBGu7NaExGGDqk-FJo0YTe!cCeQWK3;*)rXCB4RDH$=-Nt52 z?3$Yf$YW9cq+T*%T9v2wG`#?BL$o;UKn{vyD+&a%3))}gGEQu*%#l^bs;$3P@Hkp8 z{sGraDi=aww^RF@B<2oTRT)Fpq9vf)ZZ~6KU8FFs2fnaCO4sQku9M-sbuFC!x)FLQ z(=bg+nDbr~N=Vn~WI(czP59YhlMBlS(ZJ*saaz+4xq&o#sL{I~+q82fQPVHI-&v)h zX7_~WJ*$&&W;2Ep|9Q{Uz(xKCP)}@*3ji2~*w71HiE!qFCaA`HoP24xMEDruzu=b5 zoELmp(KXlTKU+}3D4iN`e(m+R)N8^i{w;t~;&61LANQ%=6n?>7K*+Ry_Xu~aQH!q0 z008d?e=X}iK;m?0E#eSw5d@yQ_vCyN(f^4KOk$uiOQ1WHX!30-D@ZSRr)C}(#XOW= z--KiEv`t%jNoSQi)DmM>x~9KLr79 zyTL(lEe^`wLe38f>wrI5^Q~mi#;j5}VExtrmA?LxC7iSu#WVl_bp!TYmI0gkr3#%S zV1A>Jam0jqQF$9l(g$J?4j~OQ%a%V>HX$*Xm1&L&kNs;=R zNF_0fWcoFw2posDC{g0Fe{vgG4sR5!>PT{t(Hr&gTIWMrS=nS+Z|{Kl@WD-C|i3m zO2RTd0%m7{44ltf7b^SNWpN|lguA?;SR>aPb@A~a7LKxi3y0anS88}<{CIIgq0pP z+4Pcz7~Bm1a^?UC6DL zFaQ7m000S;mm2Hs&?}3wUD5a9bOfi5cGusLA-8vIUVXj(jv6W*N?T;yyZp&KHKzu< zPyw@sv>G$4A-T@9^-9yA1oag)IchRfV&MFWO*~0!Mu6}d-eJCmOeXqBzoTh;LvwPG zof=QzO}=~>KfW#a>Vj}@!M-Y|9`jJp}+P&&u$bKIQ>@0O@)0Yb-$$|M7yCR*RX|1sdxX;j)Z2ZfNHstERLJ+v{ z)p!gsLYmoCES0qZhg_sJA@+Z{P%iO5kH=IY z2u;<_B9oR_TEMnru^aQq*$=_4nA4!m(+qJAk|KGFIf(tml%YMzPRxi?s?{UB!R{r< z>v{~!p;Oe$Ejped5J3+bPN=p0AG~vW6jWn(6Ao-hWF=+6bijydnGB2s46DfR9+<}> z*%${%)ZhyZSs5O3*;E*W(_Pk>6~PB<|Cj?JPj_zNYZ1f~r*v8-9}F1C*`oCQ-CgN& zU3!%lb72vlMH_4bZZ2i~8J$Iyyehrl%<|I?r5leNdM}ZN>lihn&ZMStRfr6>$f{ii zYh*QA9@_I(+$lm6GBNzZ9261m+WTm2wM7_+6$P^Kc^8NbkDb37O4zL{jgTRi$(!?W z-ord-qTQSb?DtoZ9;GMS&IW!a>zQn|b@?IS?-XYvO;Foz0%{c@2?ZJkT2%*xASw+9 znpXQ4(+_4XDNtBC=Z#BYlm+b*MGzk2yVdxipXA~6J5qOAF5?vqm|aWxxOfipCk+7pTuGRCyztT`!E{V<0( z;aB~odki`>{F9n4)lYa0QZ4H5J2Vd%A1gq}>vIpy^Lt($rDz(MpgtSCEqt zbtokq2UB`I8(xO;t>xA)eYfnS!2){10jKKD#7PS1{!kSJ=Rk$xP#ot%Flf$)I+?;$ z+>YO3dq`jMy65P6SxrnZi|>ah5X)X>nYK|a*d-Q&YzUXmTOQ_K)61$mg$}56LOwLk zpU5{yAG1fuP%jk@T$1T#eE+>~T2+<6^yvyU5D3ZX%SY4@XzV9L=47h2$3aXPBW8O0n3Th<1)@MWBS|3W_WdF-U>(97Hi$!bkEx%t4@AuqVFAZNkZAx zb6Hmm7ITynU|!7DpTV0Zb(|U>fro0m!OjWB3XR}OYt~^Hq1sjSB!9z~H2d1*C==obE8CZr?SK#j0^gnuPSjaAeye&3 z#yL4U0g%*@B57i88{zyF9U}pE zk7l~SLSQ~875nQ$)7|+N>r>1u4B~a56S!O+8+{a5St5E`sq2g;poMY=a%OKKB)ksN z10U0uNesknt>kY-aTtXL(L47wTp1W2zJOhLClqDyXlvhM4kDXkh?1${uiH!DW+hr6F0ogl&^gQD%8V|W*YZnUaYv^tJF>Y|l_pSGbyyVIA|KE0=_XZOts=?J< zn^ypp-$Ajr`H!y@9jchKny*1T=`S@}3j!c5#@)yXhk8GUopr-wTTRIu)KM5u`}~+x zMJzza!dhD?UI|f&u}<#D@a!u_BZ0tPCPw3y8&3lo(x_x6E zD}5Q+O4s;HiVc`sw}xoN3BMGzZjEHtjk;yC1BHYi$1z$6K-q63CI4g_;@0O=;=B#k z=442XT%w3y4+Ryhz(hFsN#rsQ_36^dnNYp%%MJ@7OR|;kpX@>a3U5Y(Y$01Tc*WrB` zzLDV({H^ZB-=`|vq(*l0F-+^Kp&N#cQUq?1i512eg2C%!6h2T*~ zF+qRJ&u;kzm8_cW+FHqqVJ@IFc|YJ5z)vNOFF_QSpySR-1%An;+6U?58fN&TY3WYI zCx)@%btxkr3z3E9SC-~3A?7XgW9rO{CX9`Ygebm?U>pD<3PMY`8BsjSk_+~y$+Tk;i<(AGa$h8geV%#0A(ah*#PsSR4~kGILS1Ghx)ZL zh1CelMQerwHTles!+c<^ILNu8Wjyb`V^>C%a`B8t|2&Dr9jZ%3qiH7oK)Z+A&&TC- zp3axIo}NpjP;ZCg71X3ai%xM9-zB^a^(1dKkQ&a(CVF!<#)$25Q6XV`FTZO}AkCXDQrqFOlZQL@5^TcFInN4f$G7KwRF|w|3egXI*

QJJptjb(fWvlMLoO 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 BP)7g& literal 0 HcmV?d00001 From f493a03f56edc0d445e95c21111ef9fd33de095d Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 12 Mar 2024 01:01:03 +0530 Subject: [PATCH 23/24] fix: adding github workflow events --- .github/workflows/build-branch.yml | 32 ++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 44bae0efa..42cb18035 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -2,6 +2,27 @@ name: Branch Build on: workflow_dispatch: + inputs: + build-web: + required: false + description: "Build Web" + type: boolean + default: false + build-space: + required: false + description: "Build Space" + type: boolean + default: false + build-api: + required: false + description: "Build API" + type: boolean + default: false + build-proxy: + required: false + description: "Build Proxy" + type: boolean + default: false push: branches: - master @@ -18,15 +39,15 @@ jobs: name: Build-Push Web/Space/API/Proxy Docker Image runs-on: ubuntu-latest outputs: - gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }} + gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }} gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }} gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }} gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }} gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }} - build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }} - build_space: ${{ steps.changed_files.outputs.space_any_changed }} - build_backend: ${{ steps.changed_files.outputs.backend_any_changed }} - build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }} + build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed || github.event.inputs.build-web }} + build_space: ${{ steps.changed_files.outputs.space_any_changed || github.event.inputs.build-space }} + build_backend: ${{ steps.changed_files.outputs.backend_any_changed || github.event.inputs.build-api }} + build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed || github.event.inputs.build-proxy }} steps: - id: set_env_variables @@ -280,4 +301,3 @@ jobs: DOCKER_BUILDKIT: 1 DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} - From 6ec9c64f7ca855df05f83f5329d631006a04b1a6 Mon Sep 17 00:00:00 2001 From: sriram veeraghanta Date: Tue, 12 Mar 2024 01:06:37 +0530 Subject: [PATCH 24/24] fix: build branch optional builds workflow changes --- .github/workflows/build-branch.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-branch.yml b/.github/workflows/build-branch.yml index 42cb18035..0d8d2af09 100644 --- a/.github/workflows/build-branch.yml +++ b/.github/workflows/build-branch.yml @@ -44,10 +44,10 @@ jobs: gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }} gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }} gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }} - build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed || github.event.inputs.build-web }} - build_space: ${{ steps.changed_files.outputs.space_any_changed || github.event.inputs.build-space }} - build_backend: ${{ steps.changed_files.outputs.backend_any_changed || github.event.inputs.build-api }} - build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed || github.event.inputs.build-proxy }} + build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }} + build_space: ${{ steps.changed_files.outputs.space_any_changed }} + build_backend: ${{ steps.changed_files.outputs.backend_any_changed }} + build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }} steps: - id: set_env_variables @@ -95,7 +95,7 @@ jobs: - nginx/** branch_build_push_frontend: - if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -147,7 +147,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_space: - if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event.inputs.build-space=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -199,7 +199,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_backend: - if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event.inputs.build-api=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: @@ -251,7 +251,7 @@ jobs: DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} branch_build_push_proxy: - if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} + if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event.inputs.build-web=='true' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }} runs-on: ubuntu-20.04 needs: [branch_build_setup] env: