mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge branch 'develop' of github.com:makeplane/plane into update-file-uploads
This commit is contained in:
commit
d560d1f7df
1
.github/workflows/build-branch.yml
vendored
1
.github/workflows/build-branch.yml
vendored
@ -6,7 +6,6 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- preview
|
||||
- develop
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
|
@ -52,7 +52,7 @@ If you want more control over your data, prefer to self-host Plane, please refer
|
||||
|
||||
| Installation Methods | Documentation Link |
|
||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/docker-compose) |
|
||||
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
|
||||
|
||||
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
|
||||
|
@ -1,4 +1,4 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "0.16.0"
|
||||
"version": "0.17.0"
|
||||
}
|
||||
|
@ -5,11 +5,11 @@ x-app-env: &app-env
|
||||
- NGINX_PORT=${NGINX_PORT:-80}
|
||||
- WEB_URL=${WEB_URL:-http://localhost}
|
||||
- DEBUG=${DEBUG:-0}
|
||||
- SENTRY_DSN=${SENTRY_DSN:-""}
|
||||
- SENTRY_DSN=${SENTRY_DSN}
|
||||
- SENTRY_ENVIRONMENT=${SENTRY_ENVIRONMENT:-"production"}
|
||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-""}
|
||||
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS}
|
||||
# Gunicorn Workers
|
||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-2}
|
||||
- GUNICORN_WORKERS=${GUNICORN_WORKERS:-1}
|
||||
#DB SETTINGS
|
||||
- PGHOST=${PGHOST:-plane-db}
|
||||
- PGDATABASE=${PGDATABASE:-plane}
|
||||
@ -17,11 +17,11 @@ x-app-env: &app-env
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-plane}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-plane}
|
||||
- PGDATA=${PGDATA:-/var/lib/postgresql/data}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${PGHOST}/${PGDATABASE}}
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://plane:plane@plane-db/plane}
|
||||
# REDIS SETTINGS
|
||||
- REDIS_HOST=${REDIS_HOST:-plane-redis}
|
||||
- REDIS_PORT=${REDIS_PORT:-6379}
|
||||
- REDIS_URL=${REDIS_URL:-redis://${REDIS_HOST}:6379/}
|
||||
- REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/}
|
||||
# Application secret
|
||||
- SECRET_KEY=${SECRET_KEY:-60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b5}
|
||||
# DATA STORE SETTINGS
|
||||
@ -39,7 +39,7 @@ x-app-env: &app-env
|
||||
services:
|
||||
web:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh web/server.js web
|
||||
@ -51,7 +51,7 @@ services:
|
||||
|
||||
space:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: /usr/local/bin/start.sh space/server.js space
|
||||
@ -64,7 +64,7 @@ services:
|
||||
|
||||
api:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/takeoff
|
||||
@ -78,7 +78,7 @@ services:
|
||||
|
||||
worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/worker
|
||||
@ -91,7 +91,7 @@ services:
|
||||
|
||||
beat-worker:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: unless-stopped
|
||||
command: ./bin/beat
|
||||
@ -104,7 +104,7 @@ services:
|
||||
|
||||
migrator:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
command: >
|
||||
@ -118,7 +118,7 @@ services:
|
||||
|
||||
plane-db:
|
||||
<<: *app-env
|
||||
image: postgres:15.2-alpine
|
||||
image: postgres:15.5-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
command: postgres -c 'max_connections=1000'
|
||||
@ -126,7 +126,7 @@ services:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
plane-redis:
|
||||
<<: *app-env
|
||||
image: redis:6.2.7-alpine
|
||||
image: redis:7.2.4-alpine
|
||||
pull_policy: if_not_present
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
@ -144,7 +144,7 @@ services:
|
||||
# Comment this if you already have a reverse proxy running
|
||||
proxy:
|
||||
<<: *app-env
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-latest}
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
ports:
|
||||
- ${NGINX_PORT}:80
|
||||
|
@ -17,16 +17,16 @@ function print_header() {
|
||||
clear
|
||||
|
||||
cat <<"EOF"
|
||||
---------------------------------------
|
||||
____ _
|
||||
| _ \| | __ _ _ __ ___
|
||||
| |_) | |/ _` | '_ \ / _ \
|
||||
| __/| | (_| | | | | __/
|
||||
|_| |_|\__,_|_| |_|\___|
|
||||
|
||||
---------------------------------------
|
||||
--------------------------------------------
|
||||
____ _ /////////
|
||||
| _ \| | __ _ _ __ ___ /////////
|
||||
| |_) | |/ _` | '_ \ / _ \ ///// /////
|
||||
| __/| | (_| | | | | __/ ///// /////
|
||||
|_| |_|\__,_|_| |_|\___| ////
|
||||
////
|
||||
--------------------------------------------
|
||||
Project management tool from the future
|
||||
---------------------------------------
|
||||
--------------------------------------------
|
||||
EOF
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ function buildLocalImage() {
|
||||
cd $PLANE_TEMP_CODE_DIR
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=latest
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
|
||||
docker compose -f build.yml build --no-cache >&2
|
||||
@ -99,17 +99,17 @@ function download() {
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/docker-compose.yaml https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/docker-compose.yml?$(date +%s)
|
||||
curl -H 'Cache-Control: no-cache, no-store' -s -o $PLANE_INSTALL_DIR/variables-upgrade.env https://raw.githubusercontent.com/makeplane/plane/$BRANCH/deploy/selfhost/variables.env?$(date +%s)
|
||||
|
||||
if [ -f "$PLANE_INSTALL_DIR/.env" ];
|
||||
if [ -f "$DOCKER_ENV_PATH" ];
|
||||
then
|
||||
cp $PLANE_INSTALL_DIR/.env $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
cp $DOCKER_ENV_PATH $PLANE_INSTALL_DIR/archive/$TS.env
|
||||
else
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $PLANE_INSTALL_DIR/.env
|
||||
mv $PLANE_INSTALL_DIR/variables-upgrade.env $DOCKER_ENV_PATH
|
||||
fi
|
||||
|
||||
if [ "$BRANCH" != "master" ];
|
||||
then
|
||||
cp $PLANE_INSTALL_DIR/docker-compose.yaml $PLANE_INSTALL_DIR/temp.yaml
|
||||
sed -e 's@${APP_RELEASE:-latest}@'"$BRANCH"'@g' \
|
||||
sed -e 's@${APP_RELEASE:-stable}@'"$BRANCH"'@g' \
|
||||
$PLANE_INSTALL_DIR/temp.yaml > $PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
|
||||
rm $PLANE_INSTALL_DIR/temp.yaml
|
||||
@ -131,9 +131,9 @@ function download() {
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Latest version is now available for you to use"
|
||||
echo "Most recent Stable version is now available for you to use"
|
||||
echo ""
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in '.env 'file."
|
||||
echo "In case of Upgrade, your new setting file is availabe as 'variables-upgrade.env'. Please compare and set the required values in 'plane.env 'file."
|
||||
echo ""
|
||||
|
||||
}
|
||||
@ -144,7 +144,7 @@ function startServices() {
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
local idx=0
|
||||
while docker inspect --format='{{.State.Status}}' $migrator_container_id | grep -q "running"; do
|
||||
local message=">>> Waiting for Data Migration to finish"
|
||||
local message=">> Waiting for Data Migration to finish"
|
||||
local dots=$(printf '%*s' $idx | tr ' ' '.')
|
||||
echo -ne "\r$message$dots"
|
||||
((idx++))
|
||||
@ -152,13 +152,18 @@ function startServices() {
|
||||
done
|
||||
fi
|
||||
printf "\r\033[K"
|
||||
echo ""
|
||||
echo " Data Migration completed successfully ✅"
|
||||
|
||||
# if migrator exit status is not 0, show error message and exit
|
||||
if [ -n "$migrator_container_id" ]; then
|
||||
local migrator_exit_code=$(docker inspect --format='{{.State.ExitCode}}' $migrator_container_id)
|
||||
if [ $migrator_exit_code -ne 0 ]; then
|
||||
echo "Plane Server failed to start ❌"
|
||||
stopServices
|
||||
# stopServices
|
||||
echo
|
||||
echo "Please check the logs for the 'migrator' service and resolve the issue(s)."
|
||||
echo "Stop the services by running the command: ./setup.sh stop"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@ -167,26 +172,35 @@ function startServices() {
|
||||
local idx2=0
|
||||
while ! docker logs $api_container_id 2>&1 | grep -m 1 -i "Application startup complete" | grep -q ".";
|
||||
do
|
||||
local message=">>> Waiting for API Service to Start"
|
||||
local message=">> Waiting for API Service to Start"
|
||||
local dots=$(printf '%*s' $idx2 | tr ' ' '.')
|
||||
echo -ne "\r$message$dots"
|
||||
((idx2++))
|
||||
sleep 1
|
||||
done
|
||||
printf "\r\033[K"
|
||||
echo " API Service started successfully ✅"
|
||||
source "${DOCKER_ENV_PATH}"
|
||||
echo " Plane Server started successfully ✅"
|
||||
echo ""
|
||||
echo " You can access the application at $WEB_URL"
|
||||
echo ""
|
||||
|
||||
}
|
||||
function stopServices() {
|
||||
docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH down
|
||||
}
|
||||
function restartServices() {
|
||||
docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH restart
|
||||
# docker compose -f $DOCKER_FILE_PATH --env-file=$DOCKER_ENV_PATH restart
|
||||
stopServices
|
||||
startServices
|
||||
}
|
||||
function upgrade() {
|
||||
echo "***** STOPPING SERVICES ****"
|
||||
stopServices
|
||||
|
||||
echo
|
||||
echo "***** DOWNLOADING LATEST VERSION ****"
|
||||
echo "***** DOWNLOADING STABLE VERSION ****"
|
||||
download
|
||||
|
||||
echo "***** PLEASE VALIDATE AND START SERVICES ****"
|
||||
@ -303,15 +317,15 @@ function askForAction() {
|
||||
elif [ "$ACTION" == "2" ] || [ "$DEFAULT_ACTION" == "start" ]
|
||||
then
|
||||
startServices
|
||||
askForAction
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "3" ] || [ "$DEFAULT_ACTION" == "stop" ]
|
||||
then
|
||||
stopServices
|
||||
askForAction
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "4" ] || [ "$DEFAULT_ACTION" == "restart" ]
|
||||
then
|
||||
restartServices
|
||||
askForAction
|
||||
# askForAction
|
||||
elif [ "$ACTION" == "5" ] || [ "$DEFAULT_ACTION" == "upgrade" ]
|
||||
then
|
||||
upgrade
|
||||
@ -343,7 +357,7 @@ fi
|
||||
|
||||
if [ "$BRANCH" == "master" ];
|
||||
then
|
||||
export APP_RELEASE=latest
|
||||
export APP_RELEASE=stable
|
||||
fi
|
||||
|
||||
# REMOVE SPECIAL CHARACTERS FROM BRANCH NAME
|
||||
@ -354,7 +368,21 @@ fi
|
||||
mkdir -p $PLANE_INSTALL_DIR/archive
|
||||
|
||||
DOCKER_FILE_PATH=$PLANE_INSTALL_DIR/docker-compose.yaml
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
|
||||
DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/plane.env
|
||||
|
||||
# BACKWARD COMPATIBILITY
|
||||
OLD_DOCKER_ENV_PATH=$PLANE_INSTALL_DIR/.env
|
||||
if [ -f "$OLD_DOCKER_ENV_PATH" ];
|
||||
then
|
||||
mv "$OLD_DOCKER_ENV_PATH" "$DOCKER_ENV_PATH"
|
||||
OS_NAME=$(uname)
|
||||
if [ "$OS_NAME" == "Darwin" ];
|
||||
then
|
||||
sed -i '' -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
else
|
||||
sed -i -e 's@APP_RELEASE=latest@APP_RELEASE=stable@' "$DOCKER_ENV_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header
|
||||
askForAction $@
|
||||
|
@ -1,4 +1,4 @@
|
||||
APP_RELEASE=latest
|
||||
APP_RELEASE=stable
|
||||
|
||||
WEB_REPLICAS=1
|
||||
SPACE_REPLICAS=1
|
||||
@ -41,4 +41,4 @@ BUCKET_NAME=uploads
|
||||
FILE_SIZE_LIMIT=5242880
|
||||
|
||||
# Gunicorn Workers
|
||||
GUNICORN_WORKERS=2
|
||||
GUNICORN_WORKERS=1
|
||||
|
@ -39,7 +39,7 @@ http {
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_pass http://plane-minio:9000/uploads/;
|
||||
proxy_pass http://plane-minio:9000/${BUCKET_NAME}/;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-core",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { FC, ReactNode } from "react";
|
||||
import { cn } from "src/lib/utils";
|
||||
|
||||
interface EditorContainerProps {
|
||||
editor: Editor | null;
|
||||
@ -53,7 +54,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
onMouseLeave={() => {
|
||||
hideDragHandle?.();
|
||||
}}
|
||||
className={`cursor-text ${editorClassNames}`}
|
||||
className={cn(`cursor-text`, { "active-editor": editor?.isFocused && editor?.isEditable }, editorClassNames)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -22,7 +22,7 @@ export const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
// @ts-ignore
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.querySelector("#editor-container"),
|
||||
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
|
||||
content: reactRenderer.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/document-editor",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "Package that powers Plane's Pages Editor",
|
||||
"main": "./dist/index.mjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor-extensions",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "Package that powers Plane's Editor with extensions",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
@ -330,7 +330,7 @@ const renderItems = () => {
|
||||
// @ts-ignore
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.querySelector("#editor-container"),
|
||||
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/lite-text-editor",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "Package that powers Plane's Comment Editor",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/rich-text-editor",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "Rich Text Editor that powers Plane",
|
||||
"private": true,
|
||||
"main": "./dist/index.mjs",
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eslint-config-custom",
|
||||
"private": true,
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {},
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tailwind-config-custom",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"description": "common tailwind configuration across monorepo",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tsconfig",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/types",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"main": "./src/index.d.ts"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "@plane/ui",
|
||||
"description": "UI components shared across multiple apps internally",
|
||||
"private": true,
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
@ -91,7 +91,7 @@ const CustomSelect = (props: ICustomSelectProps) => {
|
||||
)}
|
||||
</>
|
||||
{isOpen && (
|
||||
<Listbox.Options className="fixed z-10" onClick={() => closeDropdown()} static>
|
||||
<Listbox.Options className="fixed z-20" onClick={() => closeDropdown()} static>
|
||||
<div
|
||||
className={cn(
|
||||
"my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-[12rem] whitespace-nowrap",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
@ -1,9 +1,7 @@
|
||||
// icons
|
||||
import { Triangle } from "lucide-react";
|
||||
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
|
||||
// types
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
import { IDefaultAnalyticsResponse, TStateGroups } from "@plane/types";
|
||||
// constants
|
||||
import { STATE_GROUPS } from "@/constants/state";
|
||||
|
||||
type Props = {
|
||||
defaultAnalytics: IDefaultAnalyticsResponse;
|
||||
@ -16,7 +14,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
<h4 className="text-base font-medium text-custom-text-100">Total open tasks</h4>
|
||||
<h3 className="mt-1 text-xl font-semibold">{defaultAnalytics.open_issues}</h3>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 pb-2">
|
||||
{defaultAnalytics?.open_issues_classified.map((group) => {
|
||||
const percentage = ((group.state_count / defaultAnalytics.total_issues) * 100).toFixed(0);
|
||||
|
||||
@ -50,14 +48,5 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="!mt-6 flex w-min items-center gap-2 whitespace-nowrap rounded-md border border-custom-border-200 bg-custom-background-80 p-2 text-xs">
|
||||
<p className="flex items-center gap-1 text-custom-text-200">
|
||||
<Triangle className="h-4 w-4" />
|
||||
<span>Estimate Demand:</span>
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{defaultAnalytics.open_estimate_sum}/{defaultAnalytics.total_estimate_sum}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -69,7 +69,9 @@ export const CommandPaletteHelpActions: React.FC<Props> = (props) => {
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
closePalette();
|
||||
(window as any)?.$crisp.push(["do", "chat:open"]);
|
||||
if (window) {
|
||||
window.$crisp.push(["do", "chat:show"]);
|
||||
}
|
||||
}}
|
||||
className="focus:outline-none"
|
||||
>
|
||||
|
@ -40,7 +40,7 @@ const issueService = new IssueService();
|
||||
|
||||
export const CommandModal: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { getProjectById, workspaceProjectIds } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// states
|
||||
const [placeholder, setPlaceholder] = useState("Type a command or search...");
|
||||
@ -282,6 +282,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
setSearchTerm={(newSearchTerm) => setSearchTerm(newSearchTerm)}
|
||||
/>
|
||||
)}
|
||||
{workspaceSlug && workspaceProjectIds && workspaceProjectIds.length > 0 && (
|
||||
<Command.Group heading="Issue">
|
||||
<Command.Item
|
||||
onSelect={() => {
|
||||
@ -298,6 +299,7 @@ export const CommandModal: React.FC = observer(() => {
|
||||
<kbd>C</kbd>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
{workspaceSlug && (
|
||||
<Command.Group heading="Project">
|
||||
|
@ -36,7 +36,8 @@ export const ShortcutsModal: FC<Props> = (props) => {
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-20 h-full w-full overflow-y-auto">
|
||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -46,8 +47,7 @@ export const ShortcutsModal: FC<Props> = (props) => {
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="h-full w-full">
|
||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-20">
|
||||
<Dialog.Panel className="relative flex h-full items-center justify-center">
|
||||
<div className="flex h-[61vh] w-full flex-col space-y-4 overflow-hidden rounded-lg bg-custom-background-100 p-5 shadow-custom-shadow-md transition-all sm:w-[28rem]">
|
||||
<Dialog.Title as="h3" className="flex justify-between">
|
||||
<span className="text-lg font-medium">Keyboard shortcuts</span>
|
||||
@ -71,10 +71,10 @@ export const ShortcutsModal: FC<Props> = (props) => {
|
||||
</div>
|
||||
<ShortcutCommandsList searchQuery={query} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
import { LayoutGrid, Zap } from "lucide-react";
|
||||
import { Home, Zap } from "lucide-react";
|
||||
// images
|
||||
import githubBlackImage from "/public/logos/github-black.png";
|
||||
import githubWhiteImage from "/public/logos/github-white.png";
|
||||
@ -25,9 +25,7 @@ export const WorkspaceDashboardHeader = () => {
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink label="Dashboard" icon={<LayoutGrid className="h-4 w-4 text-custom-text-300" />} />
|
||||
}
|
||||
link={<BreadcrumbLink label="Home" icon={<Home className="h-4 w-4 text-custom-text-300" />} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { Copy, Pencil, Trash2 } from "lucide-react";
|
||||
// icons
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues";
|
||||
// constant
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUser } from "@/hooks/store";
|
||||
// ui
|
||||
// components
|
||||
// helpers
|
||||
// types
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// constant
|
||||
|
||||
export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props;
|
||||
@ -89,19 +90,6 @@ export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
setCreateUpdateIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Copy className="h-3 w-3" />
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isDeletingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
|
@ -84,7 +84,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||
|
||||
const payload = {
|
||||
...changesMade,
|
||||
name: changesMade?.name && changesMade?.name?.trim() === "" ? changesMade.name?.trim() : "Untitled",
|
||||
name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled",
|
||||
};
|
||||
|
||||
await issueDraftService
|
||||
|
@ -26,6 +26,7 @@ import { renderFormattedPayloadDate, getDate } from "@/helpers/date-time.helper"
|
||||
import { getChangedIssuefields } from "@/helpers/issue.helper";
|
||||
import { shouldRenderProject } from "@/helpers/project.helper";
|
||||
import { useApplication, useEstimate, useIssueDetail, useMention, useProject, useWorkspace } from "@/hooks/store";
|
||||
import { useProjectIssueProperties } from "@/hooks/use-project-issue-properties";
|
||||
// services
|
||||
import { AIService } from "@/services/ai.service";
|
||||
import { FileService } from "@/services/file.service";
|
||||
@ -121,6 +122,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
// store hooks
|
||||
const {
|
||||
config: { envConfig },
|
||||
router: { projectId: routeProjectId },
|
||||
} = useApplication();
|
||||
const { getProjectById } = useProject();
|
||||
const { areEstimatesEnabledForProject } = useEstimate();
|
||||
@ -128,6 +130,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { fetchCycles } = useProjectIssueProperties();
|
||||
// form info
|
||||
const {
|
||||
formState: { errors, isDirty, isSubmitting, dirtyFields },
|
||||
@ -160,6 +163,7 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||
parent_id: formData.parent_id,
|
||||
});
|
||||
}
|
||||
if (projectId && routeProjectId !== projectId) fetchCycles(workspaceSlug, projectId);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [projectId]);
|
||||
|
||||
|
@ -10,11 +10,11 @@ import {
|
||||
ContrastIcon,
|
||||
FileText,
|
||||
LayersIcon,
|
||||
LayoutGrid,
|
||||
PenSquare,
|
||||
Search,
|
||||
Settings,
|
||||
Bell,
|
||||
Home,
|
||||
} from "lucide-react";
|
||||
import { IWorkspace } from "@plane/types";
|
||||
import { Avatar, DiceIcon, PhotoFilterIcon } from "@plane/ui";
|
||||
@ -26,8 +26,8 @@ import projectEmoji from "public/emoji/project-emoji.svg";
|
||||
|
||||
const workspaceLinks = [
|
||||
{
|
||||
Icon: LayoutGrid,
|
||||
name: "Dashboard",
|
||||
Icon: Home,
|
||||
name: "Home",
|
||||
},
|
||||
{
|
||||
Icon: BarChart2,
|
||||
|
@ -226,7 +226,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
|
||||
control={control}
|
||||
name="name"
|
||||
rules={{
|
||||
required: "Title is required",
|
||||
required: "Name is required",
|
||||
maxLength: {
|
||||
value: 255,
|
||||
message: "Title should be less than 255 characters",
|
||||
@ -240,7 +240,7 @@ export const CreateProjectForm: FC<Props> = observer((props) => {
|
||||
value={value}
|
||||
onChange={handleNameChange(onChange)}
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Project title"
|
||||
placeholder="Project name"
|
||||
className="w-full focus:border-blue-400"
|
||||
tabIndex={1}
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
// icons
|
||||
import { Lock } from "lucide-react";
|
||||
import { Info, Lock } from "lucide-react";
|
||||
import { IProject, IWorkspace } from "@plane/types";
|
||||
// ui
|
||||
import {
|
||||
@ -13,6 +13,7 @@ import {
|
||||
setToast,
|
||||
CustomEmojiIconPicker,
|
||||
EmojiIconPickerTypes,
|
||||
Tooltip,
|
||||
} from "@plane/ui";
|
||||
// components
|
||||
import { ImagePickerPopover } from "@/components/core";
|
||||
@ -24,6 +25,7 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { convertHexEmojiToDecimal } from "@/helpers/emoji.helper";
|
||||
import { useEventTracker, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project";
|
||||
// types
|
||||
@ -42,6 +44,7 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
// store hooks
|
||||
const { captureProjectEvent } = useEventTracker();
|
||||
const { updateProject } = useProject();
|
||||
const { isMobile } = usePlatformOS();
|
||||
// form info
|
||||
const {
|
||||
handleSubmit,
|
||||
@ -229,6 +232,9 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-xs text-red-500">
|
||||
<>{errors?.name?.message}</>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm">Description</h4>
|
||||
@ -249,22 +255,25 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full items-center justify-between gap-10">
|
||||
<div className="flex w-full justify-between gap-10">
|
||||
<div className="flex w-1/2 flex-col gap-1">
|
||||
<h4 className="text-sm">Identifier</h4>
|
||||
<h4 className="text-sm">Project ID</h4>
|
||||
<div className="relative">
|
||||
<Controller
|
||||
control={control}
|
||||
name="identifier"
|
||||
rules={{
|
||||
required: "Identifier is required",
|
||||
validate: (value) => /^[A-Z0-9]+$/.test(value.toUpperCase()) || "Identifier must be in uppercase.",
|
||||
required: "Project ID is required",
|
||||
validate: (value) =>
|
||||
/^[ÇŞĞIİÖÜA-Z0-9]+$/.test(value.toUpperCase()) ||
|
||||
"Only Alphanumeric & Non-latin characters are allowed.",
|
||||
minLength: {
|
||||
value: 1,
|
||||
message: "Identifier must at least be of 1 character",
|
||||
message: "Project ID must at least be of 1 character",
|
||||
},
|
||||
maxLength: {
|
||||
value: 12,
|
||||
message: "Identifier must at most be of 5 characters",
|
||||
value: 5,
|
||||
message: "Project ID must at most be of 5 characters",
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, ref } }) => (
|
||||
@ -276,12 +285,24 @@ export const ProjectDetailsForm: FC<IProjectDetailsForm> = (props) => {
|
||||
onChange={handleIdentifierChange}
|
||||
ref={ref}
|
||||
hasError={Boolean(errors.identifier)}
|
||||
placeholder="Enter identifier"
|
||||
placeholder="Enter Project ID"
|
||||
className="w-full font-medium"
|
||||
disabled={!isAdmin}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipContent="Helps you identify issues in the project uniquely, (e.g. APP-123). Max 5 characters."
|
||||
className="text-sm"
|
||||
position="right-top"
|
||||
>
|
||||
<Info className="absolute right-2 top-2.5 h-4 w-4 text-custom-text-400" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span className="text-xs text-red-500">
|
||||
<>{errors?.identifier?.message}</>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex w-1/2 flex-col gap-1">
|
||||
<h4 className="text-sm">Network</h4>
|
||||
|
89
web/hooks/use-project-issue-properties.ts
Normal file
89
web/hooks/use-project-issue-properties.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { useCycle, useEstimate, useLabel, useMember, useModule, useProjectState } from "./store";
|
||||
|
||||
export const useProjectIssueProperties = () => {
|
||||
const { fetchProjectStates } = useProjectState();
|
||||
const {
|
||||
project: { fetchProjectMembers },
|
||||
} = useMember();
|
||||
const { fetchProjectLabels } = useLabel();
|
||||
const { fetchAllCycles: fetchProjectAllCycles } = useCycle();
|
||||
const { fetchModules: fetchProjectAllModules } = useModule();
|
||||
const { fetchProjectEstimates } = useEstimate();
|
||||
|
||||
// fetching project states
|
||||
const fetchStates = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectStates(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project members
|
||||
const fetchMembers = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
|
||||
// fetching project labels
|
||||
const fetchLabels = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project cycles
|
||||
const fetchCycles = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectAllCycles(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project modules
|
||||
const fetchModules = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectAllModules(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
// fetching project estimates
|
||||
const fetchEstimates = async (
|
||||
workspaceSlug: string | string[] | undefined,
|
||||
projectId: string | string[] | undefined
|
||||
) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchProjectEstimates(workspaceSlug.toString(), projectId.toString());
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAll = async (workspaceSlug: string | string[] | undefined, projectId: string | string[] | undefined) => {
|
||||
if (workspaceSlug && projectId) {
|
||||
await fetchStates(workspaceSlug, projectId);
|
||||
await fetchMembers(workspaceSlug, projectId);
|
||||
await fetchLabels(workspaceSlug, projectId);
|
||||
await fetchCycles(workspaceSlug, projectId);
|
||||
await fetchModules(workspaceSlug, projectId);
|
||||
await fetchEstimates(workspaceSlug, projectId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
fetchAll,
|
||||
fetchStates,
|
||||
fetchMembers,
|
||||
fetchLabels,
|
||||
fetchCycles,
|
||||
fetchModules,
|
||||
fetchEstimates,
|
||||
};
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "turbo run develop",
|
||||
|
@ -14,7 +14,7 @@ import { NextPageWithLayout } from "@/lib/types";
|
||||
const WorkspacePage: NextPageWithLayout = observer(() => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
// derived values
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Dashboard` : undefined;
|
||||
const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Home` : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -220,7 +220,7 @@ const UserInvitationsPage: NextPageWithLayout = observer(() => {
|
||||
description="You can see here if someone invites you to a workspace."
|
||||
image={emptyInvitation}
|
||||
primaryButton={{
|
||||
text: "Back to dashboard",
|
||||
text: "Back to home",
|
||||
onClick: () => router.push("/"),
|
||||
}}
|
||||
/>
|
||||
|
@ -81,7 +81,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
|
||||
title={`You are already a member of ${invitationDetail.workspace.name}`}
|
||||
description="Your workspace is where you'll create projects, collaborate on your issues, and organize different streams of work in your Plane account."
|
||||
>
|
||||
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
|
||||
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
|
||||
</EmptySpace>
|
||||
</>
|
||||
) : (
|
||||
@ -105,7 +105,7 @@ const WorkspaceInvitationPage: NextPageWithLayout = observer(() => {
|
||||
{!currentUser ? (
|
||||
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
|
||||
) : (
|
||||
<EmptySpaceItem Icon={Boxes} title="Continue to Dashboard" href="/" />
|
||||
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
|
||||
)}
|
||||
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
|
||||
<EmptySpaceItem
|
||||
|
@ -2733,7 +2733,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.2.42":
|
||||
"@types/react@*", "@types/react@18.2.42", "@types/react@^18.2.42":
|
||||
version "18.2.42"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.42.tgz#6f6b11a904f6d96dda3c2920328a97011a00aba7"
|
||||
integrity sha512-c1zEr96MjakLYus/wPnuWDo1/zErfdU9rNsIGmE+NV71nx88FG9Ttgo5dqorXTu/LImX2f63WBP986gJkMPNbA==
|
||||
|
Loading…
Reference in New Issue
Block a user