fix: merge conflicts resolved from preview

This commit is contained in:
Aaryan Khandelwal 2024-05-15 16:45:28 +05:30
commit 70f21649ed
358 changed files with 5920 additions and 4088 deletions

View File

@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"], extends: ["custom"],
settings: { settings: {
next: { next: {
rootDir: ["web/", "space/"], rootDir: ["web/", "space/", "admin/"],
}, },
}, },
}; };

View File

@ -14,10 +14,10 @@ jobs:
space_changed: ${{ steps.changed-files.outputs.space_any_changed }} space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }} web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Get changed files - name: Get changed files
id: changed-files id: changed-files
uses: tj-actions/changed-files@v41 uses: tj-actions/changed-files@v44
with: with:
files_yaml: | files_yaml: |
apiserver: apiserver:
@ -49,9 +49,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: needs.get-changed-files.outputs.apiserver_changed == 'true' if: needs.get-changed-files.outputs.apiserver_changed == 'true'
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.x" # Specify the Python version you need python-version: "3.x" # Specify the Python version you need
- name: Install Pylint - name: Install Pylint
@ -66,9 +66,9 @@ jobs:
if: needs.get-changed-files.outputs.admin_changed == 'true' if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install
@ -79,9 +79,9 @@ jobs:
if: needs.get-changed-files.outputs.space_changed == 'true' if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install
@ -92,9 +92,9 @@ jobs:
if: needs.get-changed-files.outputs.web_changed == 'true' if: needs.get-changed-files.outputs.web_changed == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install
@ -104,9 +104,9 @@ jobs:
needs: lint-admin needs: lint-admin
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install
@ -116,9 +116,9 @@ jobs:
needs: lint-space needs: lint-space
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install
@ -128,9 +128,9 @@ jobs:
needs: lint-web needs: lint-web
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: yarn install - run: yarn install

View File

@ -5,17 +5,17 @@ on:
inputs: inputs:
web-build: web-build:
required: false required: false
description: 'Build Web' description: "Build Web"
type: boolean type: boolean
default: true default: true
space-build: space-build:
required: false required: false
description: 'Build Space' description: "Build Space"
type: boolean type: boolean
default: false default: false
admin-build: admin-build:
required: false required: false
description: 'Build Admin' description: "Build Admin"
type: boolean type: boolean
default: false default: false
@ -35,7 +35,7 @@ jobs:
echo "BUILD_SPACE=$BUILD_SPACE" echo "BUILD_SPACE=$BUILD_SPACE"
echo "BUILD_ADMIN=$BUILD_ADMIN" echo "BUILD_ADMIN=$BUILD_ADMIN"
outputs: outputs:
web-build: ${{ env.BUILD_WEB}} web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}} space-build: ${{env.BUILD_SPACE}}
admin-build: ${{env.BUILD_ADMIN}} admin-build: ${{env.BUILD_ADMIN}}
@ -53,7 +53,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: "18"
- name: Install AWS cli - name: Install AWS cli
run: | run: |
sudo apt-get update sudo apt-get update
@ -79,7 +79,7 @@ jobs:
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ") 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 aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space: feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }} if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build needs: setup-feature-build
@ -89,7 +89,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 NEXT_PUBLIC_SPACE_BASE_PATH: "/spaces"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs: outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }} do-build: ${{ needs.setup-feature-build.outputs.space-build }}
@ -98,7 +98,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: "18"
- name: Install AWS cli - name: Install AWS cli
run: | run: |
sudo apt-get update sudo apt-get update
@ -134,7 +134,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }} AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1 NEXT_PUBLIC_ADMIN_BASE_PATH: "/god-mode"
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }} NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs: outputs:
do-build: ${{ needs.setup-feature-build.outputs.admin-build }} do-build: ${{ needs.setup-feature-build.outputs.admin-build }}
@ -143,7 +143,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: "18"
- name: Install AWS cli - name: Install AWS cli
run: | run: |
sudo apt-get update sudo apt-get update
@ -172,7 +172,13 @@ jobs:
feature-deploy: feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }} if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }}
needs: [setup-feature-build, feature-build-web, feature-build-space, feature-build-admin] needs:
[
setup-feature-build,
feature-build-web,
feature-build-space,
feature-build-admin,
]
name: Feature Deploy name: Feature Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:

View File

@ -1,2 +1,3 @@
NEXT_PUBLIC_APP_URL= NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_API_BASE_URL= NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_WEB_BASE_URL=""

View File

@ -10,5 +10,43 @@ module.exports = {
}, },
}, },
}, },
rules: {} rules: {
"import/order": [
"error",
{
groups: ["builtin", "external", "internal", "parent", "sibling",],
pathGroups: [
{
pattern: "react",
group: "external",
position: "before",
},
{
pattern: "lucide-react",
group: "external",
position: "after",
},
{
pattern: "@headlessui/**",
group: "external",
position: "after",
},
{
pattern: "@plane/**",
group: "external",
position: "after",
},
{
pattern: "@/**",
group: "internal",
}
],
pathGroupsExcludedImportTypes: ["builtin", "internal", "react"],
alphabetize: {
order: "asc",
caseInsensitive: true,
},
},
],
},
} }

View File

@ -1,3 +1,6 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
WORKDIR /app WORKDIR /app
@ -7,6 +10,9 @@ COPY . .
RUN turbo prune --scope=admin --docker RUN turbo prune --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat RUN apk add --no-cache libc6-compat
@ -21,13 +27,25 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
RUN yarn turbo run build --filter=admin RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner FROM node:18-alpine AS runner
WORKDIR /app WORKDIR /app
@ -38,12 +56,17 @@ COPY --from=installer /app/admin/.next/standalone ./
COPY --from=installer /app/admin/.next/static ./admin/.next/static COPY --from=installer /app/admin/.next/static ./admin/.next/static
COPY --from=installer /app/admin/public ./admin/public COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL="" ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1 ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1 ENV TURBO_TELEMETRY_DISABLED 1

View File

@ -3,10 +3,15 @@ RUN apk add --no-cache libc6-compat
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN yarn global add turbo RUN yarn global add turbo
RUN yarn install RUN yarn install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000 EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ] VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
CMD ["yarn", "dev", "--filter=admin"] CMD ["yarn", "dev", "--filter=admin"]

View File

@ -1 +0,0 @@
export * from "./ai-config-form";

View File

@ -1,12 +1,12 @@
import { FC } from "react"; import { FC } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Lightbulb } from "lucide-react"; import { Lightbulb } from "lucide-react";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types"; import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { ControllerInput, TControllerInputFormField } from "components/common"; import { ControllerInput, TControllerInputFormField } from "@/components/common";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
type IInstanceAIForm = { type IInstanceAIForm = {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;

View File

@ -2,20 +2,8 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
// layouts // layouts
import { AdminLayout } from "@/layouts"; import { AdminLayout } from "@/layouts/admin-layout";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
interface AILayoutProps { export default function AILayout({ children }: { children: ReactNode }) {
children: ReactNode; return <AdminLayout>{children}</AdminLayout>;
} }
const AILayout = ({ children }: AILayoutProps) => (
<InstanceWrapper>
<AuthWrapper>
<AdminLayout>{children}</AdminLayout>
</AuthWrapper>
</InstanceWrapper>
);
export default AILayout;

View File

@ -1,13 +1,14 @@
"use client"; "use client";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
import { InstanceAIForm } from "./components";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
// components
import { InstanceAIForm } from "./form";
const InstanceAIPage = observer(() => { const InstanceAIPage = observer(() => {
// store // store

View File

@ -1 +0,0 @@
export * from "./authentication-method-card";

View File

@ -3,11 +3,11 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
import { TInstanceAuthenticationMethodKeys } from "@plane/types"; import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { useInstance } from "@/hooks/store";
// ui
// types
type Props = { type Props = {
disabled: boolean; disabled: boolean;

View File

@ -1,18 +1,18 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks import Link from "next/link";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons // icons
import { Settings2 } from "lucide-react"; import { Settings2 } from "lucide-react";
// types // types
import { TInstanceAuthenticationMethodKeys } from "@plane/types"; import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = { type Props = {
disabled: boolean; disabled: boolean;

View File

@ -1,18 +1,18 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks import Link from "next/link";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons // icons
import { Settings2 } from "lucide-react"; import { Settings2 } from "lucide-react";
// types // types
import { TInstanceAuthenticationMethodKeys } from "@plane/types"; import { TInstanceAuthenticationMethodKeys } from "@plane/types";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// helpers // helpers
import { cn } from "helpers/common.helper"; import { cn } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
type Props = { type Props = {
disabled: boolean; disabled: boolean;

View File

@ -1,3 +1,5 @@
export * from "./common";
export * from "./email-config-switch"; export * from "./email-config-switch";
export * from "./password-config-switch"; export * from "./password-config-switch";
export * from "./authentication-method-card";
export * from "./github-config";
export * from "./google-config";

View File

@ -3,11 +3,11 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
import { TInstanceAuthenticationMethodKeys } from "@plane/types"; import { TInstanceAuthenticationMethodKeys } from "@plane/types";
import { ToggleSwitch } from "@plane/ui";
import { useInstance } from "@/hooks/store";
// ui
// types
type Props = { type Props = {
disabled: boolean; disabled: boolean;

View File

@ -1,2 +0,0 @@
export * from "./root";
export * from "./github-config-form";

View File

@ -1,8 +1,9 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { useForm } from "react-hook-form"; import isEmpty from "lodash/isEmpty";
import Link from "next/link"; import Link from "next/link";
// hooks import { useForm } from "react-hook-form";
import { useInstance } from "@/hooks"; // types
import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components // components
@ -12,12 +13,11 @@ import {
CopyField, CopyField,
TControllerInputFormField, TControllerInputFormField,
TCopyField, TCopyField,
} from "components/common"; } from "@/components/common";
// types
import { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
// helpers // helpers
import { API_BASE_URL, cn } from "helpers/common.helper"; import { API_BASE_URL, cn } from "@/helpers/common.helper";
import isEmpty from "lodash/isEmpty"; // hooks
import { useInstance } from "@/hooks/store";
type Props = { type Props = {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;
@ -46,7 +46,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : ""; const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const githubFormFields: TControllerInputFormField[] = [ const GITHUB_FORM_FIELDS: TControllerInputFormField[] = [
{ {
key: "GITHUB_CLIENT_ID", key: "GITHUB_CLIENT_ID",
type: "text", type: "text",
@ -55,6 +55,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<> <>
You will get this from your{" "} You will get this from your{" "}
<a <a
tabIndex={-1}
href="https://github.com/settings/applications/new" href="https://github.com/settings/applications/new"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -76,6 +77,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<> <>
Your client secret is also found in your{" "} Your client secret is also found in your{" "}
<a <a
tabIndex={-1}
href="https://github.com/settings/applications/new" href="https://github.com/settings/applications/new"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -91,7 +93,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
}, },
]; ];
const githubCopyFields: TCopyField[] = [ const GITHUB_SERVICE_FIELD: TCopyField[] = [
{ {
key: "Origin_URL", key: "Origin_URL",
label: "Origin URL", label: "Origin URL",
@ -100,6 +102,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<> <>
We will auto-generate this. Paste this into the Authorized origin URL field{" "} We will auto-generate this. Paste this into the Authorized origin URL field{" "}
<a <a
tabIndex={-1}
href="https://github.com/settings/applications/new" href="https://github.com/settings/applications/new"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -118,6 +121,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<> <>
We will auto-generate this. Paste this into your Authorized Callback URI field{" "} We will auto-generate this. Paste this into your Authorized Callback URI field{" "}
<a <a
tabIndex={-1}
href="https://github.com/settings/applications/new" href="https://github.com/settings/applications/new"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -134,13 +138,16 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
const payload: Partial<GithubConfigFormValues> = { ...formData }; const payload: Partial<GithubConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload) await updateInstanceConfigurations(payload)
.then(() => { .then((response = []) => {
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: "Success", title: "Success",
message: "Github Configuration Settings updated successfully", message: "Github Configuration Settings updated successfully",
}); });
reset(); reset({
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
});
}) })
.catch((err) => console.error(err)); .catch((err) => console.error(err));
}; };
@ -163,7 +170,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full"> <div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1"> <div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
<div className="pt-2 text-xl font-medium">Configuration</div> <div className="pt-2 text-xl font-medium">Configuration</div>
{githubFormFields.map((field) => ( {GITHUB_FORM_FIELDS.map((field) => (
<ControllerInput <ControllerInput
key={field.key} key={field.key}
control={control} control={control}
@ -194,7 +201,7 @@ export const InstanceGithubConfigForm: FC<Props> = (props) => {
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg"> <div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Service provider details</div> <div className="pt-2 text-xl font-medium">Service provider details</div>
{githubCopyFields.map((field) => ( {GITHUB_SERVICE_FIELD.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} /> <CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))} ))}
</div> </div>

View File

@ -1,22 +1,23 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import useSWR from "swr"; import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui"; import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks";
// helpers // helpers
import { resolveGeneralTheme } from "@/helpers/common.helper"; import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
import { useInstance } from "@/hooks/store";
// icons // icons
import githubLightModeImage from "@/public/logos/github-black.png"; import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png"; import githubDarkModeImage from "@/public/logos/github-white.png";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./form";
const InstanceGithubAuthenticationPage = observer(() => { const InstanceGithubAuthenticationPage = observer(() => {
// store // store

View File

@ -1,2 +0,0 @@
export * from "./root";
export * from "./google-config-form";

View File

@ -1,8 +1,9 @@
import { FC, useState } from "react"; import { FC, useState } from "react";
import { useForm } from "react-hook-form"; import isEmpty from "lodash/isEmpty";
import Link from "next/link"; import Link from "next/link";
// hooks import { useForm } from "react-hook-form";
import { useInstance } from "@/hooks"; // types
import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
// ui // ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components // components
@ -12,12 +13,11 @@ import {
CopyField, CopyField,
TControllerInputFormField, TControllerInputFormField,
TCopyField, TCopyField,
} from "components/common"; } from "@/components/common";
// types
import { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
// helpers // helpers
import { API_BASE_URL, cn } from "helpers/common.helper"; import { API_BASE_URL, cn } from "@/helpers/common.helper";
import isEmpty from "lodash/isEmpty"; // hooks
import { useInstance } from "@/hooks/store";
type Props = { type Props = {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;
@ -46,7 +46,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : ""; const originURL = !isEmpty(API_BASE_URL) ? API_BASE_URL : typeof window !== "undefined" ? window.location.origin : "";
const googleFormFields: TControllerInputFormField[] = [ const GOOGLE_FORM_FIELDS: TControllerInputFormField[] = [
{ {
key: "GOOGLE_CLIENT_ID", key: "GOOGLE_CLIENT_ID",
type: "text", type: "text",
@ -55,6 +55,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
<> <>
Your client ID lives in your Google API Console.{" "} Your client ID lives in your Google API Console.{" "}
<a <a
tabIndex={-1}
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred" href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -76,6 +77,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
<> <>
Your client secret should also be in your Google API Console.{" "} Your client secret should also be in your Google API Console.{" "}
<a <a
tabIndex={-1}
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid" href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
target="_blank" target="_blank"
className="text-custom-primary-100 hover:underline" className="text-custom-primary-100 hover:underline"
@ -91,7 +93,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
}, },
]; ];
const googleCopyFeilds: TCopyField[] = [ const GOOGLE_SERVICE_DETAILS: TCopyField[] = [
{ {
key: "Origin_URL", key: "Origin_URL",
label: "Origin URL", label: "Origin URL",
@ -134,13 +136,16 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
const payload: Partial<GoogleConfigFormValues> = { ...formData }; const payload: Partial<GoogleConfigFormValues> = { ...formData };
await updateInstanceConfigurations(payload) await updateInstanceConfigurations(payload)
.then(() => { .then((response = []) => {
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: "Success", title: "Success",
message: "Google Configuration Settings updated successfully", message: "Google Configuration Settings updated successfully",
}); });
reset(); reset({
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
});
}) })
.catch((err) => console.error(err)); .catch((err) => console.error(err));
}; };
@ -163,7 +168,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full"> <div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1"> <div className="flex flex-col gap-y-4 col-span-2 md:col-span-1">
<div className="pt-2 text-xl font-medium">Configuration</div> <div className="pt-2 text-xl font-medium">Configuration</div>
{googleFormFields.map((field) => ( {GOOGLE_FORM_FIELDS.map((field) => (
<ControllerInput <ControllerInput
key={field.key} key={field.key}
control={control} control={control}
@ -194,7 +199,7 @@ export const InstanceGoogleConfigForm: FC<Props> = (props) => {
<div className="col-span-2 md:col-span-1"> <div className="col-span-2 md:col-span-1">
<div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg"> <div className="flex flex-col gap-y-4 px-6 py-4 my-2 bg-custom-background-80/60 rounded-lg">
<div className="pt-2 text-xl font-medium">Service provider details</div> <div className="pt-2 text-xl font-medium">Service provider details</div>
{googleCopyFeilds.map((field) => ( {GOOGLE_SERVICE_DETAILS.map((field) => (
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} /> <CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
))} ))}
</div> </div>

View File

@ -1,18 +1,19 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Image from "next/image";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import Image from "next/image";
import useSWR from "swr"; import useSWR from "swr";
import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui"; import { Loader, ToggleSwitch, setPromiseToast } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./components";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
// icons // icons
import GoogleLogo from "@/public/logos/google-logo.svg"; import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./form";
const InstanceGoogleAuthenticationPage = observer(() => { const InstanceGoogleAuthenticationPage = observer(() => {
// store // store

View File

@ -2,20 +2,8 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
// layouts // layouts
import { AdminLayout } from "@/layouts"; import { AdminLayout } from "@/layouts/admin-layout";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
interface AuthenticationLayoutProps { export default function AuthenticationLayout({ children }: { children: ReactNode }) {
children: ReactNode; return <AdminLayout>{children}</AdminLayout>;
} }
const AuthenticationLayout = ({ children }: AuthenticationLayoutProps) => (
<InstanceWrapper>
<AuthWrapper>
<AdminLayout>{children}</AdminLayout>
</AuthWrapper>
</InstanceWrapper>
);
export default AuthenticationLayout;

View File

@ -1,26 +1,31 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react-lite";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import useSWR from "swr"; import useSWR from "swr";
import { observer } from "mobx-react-lite";
import { Mails, KeyRound } from "lucide-react"; import { Mails, KeyRound } from "lucide-react";
import { Loader, setPromiseToast } from "@plane/ui";
import { TInstanceConfigurationKeys } from "@plane/types"; import { TInstanceConfigurationKeys } from "@plane/types";
import { Loader, setPromiseToast } from "@plane/ui";
// components // components
import { AuthenticationMethodCard, EmailCodesConfiguration, PasswordLoginConfiguration } from "./components";
import { GoogleConfiguration } from "./google/components";
import { GithubConfiguration } from "./github/components";
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
// hooks // hooks
import { useInstance } from "@/hooks";
// helpers // helpers
import { resolveGeneralTheme } from "@/helpers/common.helper"; import { resolveGeneralTheme } from "@/helpers/common.helper";
import { useInstance } from "@/hooks/store";
// images // images
import GoogleLogo from "@/public/logos/google-logo.svg";
import githubLightModeImage from "@/public/logos/github-black.png"; import githubLightModeImage from "@/public/logos/github-black.png";
import githubDarkModeImage from "@/public/logos/github-white.png"; import githubDarkModeImage from "@/public/logos/github-white.png";
import GoogleLogo from "@/public/logos/google-logo.svg";
// local components
import {
AuthenticationMethodCard,
EmailCodesConfiguration,
PasswordLoginConfiguration,
GithubConfiguration,
GoogleConfiguration,
} from "./components";
type TInstanceAuthenticationMethodCard = { type TInstanceAuthenticationMethodCard = {
key: string; key: string;

View File

@ -1,14 +1,14 @@
import React, { FC, useMemo, useState } from "react"; import React, { FC, useMemo, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// hooks // hooks
import { useInstance } from "@/hooks"; import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
// ui
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
import { useInstance } from "@/hooks/store";
// ui
// components // components
import { ControllerInput, TControllerInputFormField } from "components/common"; import { ControllerInput, TControllerInputFormField } from "components/common";
import { SendTestEmailModal } from "./test-email-modal"; import { SendTestEmailModal } from "./test-email-modal";
// types // types
import { IFormattedInstanceConfiguration, TInstanceEmailConfigurationKeys } from "@plane/types";
type IInstanceEmailForm = { type IInstanceEmailForm = {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;

View File

@ -2,7 +2,7 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
// layouts // layouts
import { AdminLayout } from "@/layouts"; import { AdminLayout } from "@/layouts/admin-layout";
// lib // lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers"; import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";

View File

@ -1,13 +1,13 @@
"use client"; "use client";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
import { useInstance } from "@/hooks/store";
import { InstanceEmailForm } from "./components"; import { InstanceEmailForm } from "./components";
// hooks // hooks
import { useInstance } from "@/hooks";
const InstanceEmailPage = observer(() => { const InstanceEmailPage = observer(() => {
// store // store

9
admin/app/error.tsx Normal file
View File

@ -0,0 +1,9 @@
"use client";
export default function RootErrorPage() {
return (
<div>
<p>Something went wrong.</p>
</div>
);
}

View File

@ -1 +0,0 @@
export * from "./general-config-form";

View File

@ -1,19 +1,23 @@
"use client";
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { Telescope } from "lucide-react"; import { Telescope } from "lucide-react";
// types
import { IInstance, IInstanceAdmin } from "@plane/types"; import { IInstance, IInstanceAdmin } from "@plane/types";
// ui
import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// components // components
import { ControllerInput } from "components/common"; import { ControllerInput } from "@/components/common";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
export interface IGeneralConfigurationForm { export interface IGeneralConfigurationForm {
instance: IInstance["instance"]; instance: IInstance["instance"];
instanceAdmins: IInstanceAdmin[]; instanceAdmins: IInstanceAdmin[];
} }
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = (props) => { export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
const { instance, instanceAdmins } = props; const { instance, instanceAdmins } = props;
// hooks // hooks
const { updateInstanceInfo } = useInstance(); const { updateInstanceInfo } = useInstance();
@ -24,8 +28,8 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = (props) =
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<Partial<IInstance["instance"]>>({ } = useForm<Partial<IInstance["instance"]>>({
defaultValues: { defaultValues: {
instance_name: instance.instance_name, instance_name: instance?.instance_name,
is_telemetry_enabled: instance.is_telemetry_enabled, is_telemetry_enabled: instance?.is_telemetry_enabled,
}, },
}); });
@ -133,4 +137,4 @@ export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = (props) =
</div> </div>
</div> </div>
); );
}; });

View File

@ -1,21 +1,12 @@
"use client";
import { ReactNode } from "react"; import { ReactNode } from "react";
// layouts import { Metadata } from "next";
import { AdminLayout } from "@/layouts"; // components
// lib import { AdminLayout } from "@/layouts/admin-layout";
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
interface GeneralLayoutProps { export const metadata: Metadata = {
children: ReactNode; title: "General Settings - God Mode",
};
export default function GeneralLayout({ children }: { children: ReactNode }) {
return <AdminLayout>{children}</AdminLayout>;
} }
const GeneralLayout = ({ children }: GeneralLayoutProps) => (
<InstanceWrapper>
<AuthWrapper>
<AdminLayout>{children}</AdminLayout>
</AuthWrapper>
</InstanceWrapper>
);
export default GeneralLayout;

View File

@ -1,18 +1,15 @@
"use client"; "use client";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// components
import { PageHeader } from "@/components/core";
import { GeneralConfigurationForm } from "./components";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
// components
import { GeneralConfigurationForm } from "./form";
const GeneralPage = observer(() => { function GeneralPage() {
const { instance, instanceAdmins } = useInstance(); const { instance, instanceAdmins } = useInstance();
console.log("instance", instanceAdmins);
return ( return (
<> <>
<PageHeader title="General Settings - God Mode" />
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col"> <div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0"> <div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
<div className="text-xl font-medium text-custom-text-100">General settings</div> <div className="text-xl font-medium text-custom-text-100">General settings</div>
@ -22,13 +19,13 @@ const GeneralPage = observer(() => {
</div> </div>
</div> </div>
<div className="flex-grow overflow-hidden overflow-y-auto"> <div className="flex-grow overflow-hidden overflow-y-auto">
{instance?.instance && instanceAdmins && instanceAdmins?.length > 0 && ( {instance?.instance && instanceAdmins && (
<GeneralConfigurationForm instance={instance?.instance} instanceAdmins={instanceAdmins} /> <GeneralConfigurationForm instance={instance.instance} instanceAdmins={instanceAdmins} />
)} )}
</div> </div>
</div> </div>
</> </>
); );
}); }
export default GeneralPage; export default observer(GeneralPage);

View File

@ -1 +0,0 @@
export * from "./image-config-form";

View File

@ -1,11 +1,11 @@
import { FC } from "react"; import { FC } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types"; import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from "@plane/types";
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// components // components
import { ControllerInput } from "components/common"; import { ControllerInput } from "@/components/common";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
type IInstanceImageConfigForm = { type IInstanceImageConfigForm = {
config: IFormattedInstanceConfiguration; config: IFormattedInstanceConfiguration;

View File

@ -1,21 +1,11 @@
"use client";
import { ReactNode } from "react"; import { ReactNode } from "react";
// layouts // layouts
import { AdminLayout } from "@/layouts"; import { AdminLayout } from "@/layouts/admin-layout";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
interface ImageLayoutProps { interface ImageLayoutProps {
children: ReactNode; children: ReactNode;
} }
const ImageLayout = ({ children }: ImageLayoutProps) => ( const ImageLayout = ({ children }: ImageLayoutProps) => <AdminLayout>{children}</AdminLayout>;
<InstanceWrapper>
<AuthWrapper>
<AdminLayout>{children}</AdminLayout>
</AuthWrapper>
</InstanceWrapper>
);
export default ImageLayout; export default ImageLayout;

View File

@ -1,13 +1,14 @@
"use client"; "use client";
import useSWR from "swr";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Loader } from "@plane/ui"; import { Loader } from "@plane/ui";
// components // components
import { PageHeader } from "@/components/core"; import { PageHeader } from "@/components/core";
import { InstanceImageConfigForm } from "./components";
// hooks // hooks
import { useInstance } from "@/hooks"; import { useInstance } from "@/hooks/store";
// local
import { InstanceImageConfigForm } from "./form";
const InstanceImagePage = observer(() => { const InstanceImagePage = observer(() => {
// store // store

View File

@ -1,48 +1,56 @@
"use client";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { ThemeProvider } from "next-themes"; import { Metadata } from "next";
// components
import { InstanceFailureView, InstanceNotReady } from "@/components/instance";
// helpers
import { ASSET_PREFIX } from "@/helpers/common.helper";
// lib // lib
import { StoreProvider } from "@/lib/store-context"; import { AppProvider } from "@/lib/app-providers";
import { AppWrapper } from "@/lib/wrappers";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
// styles // styles
import "./globals.css"; import "./globals.css";
// services
import { InstanceService } from "@/services";
interface RootLayoutProps { const instanceService = new InstanceService();
children: ReactNode;
}
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => { export const metadata: Metadata = {
const prefix = "/god-mode/"; title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
openGraph: {
title: "Plane | Simple, extensible, open-source project management tool.",
description:
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.",
url: "https://plane.so/",
},
keywords:
"software development, customer feedback, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration",
twitter: {
site: "@planepowers",
},
};
export default async function RootLayout({ children }: { children: ReactNode }) {
const instanceDetails = await instanceService.getInstanceInfo().catch(() => null);
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<title>{SITE_TITLE}</title> <link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
<meta property="og:site_name" content={SITE_NAME} /> <link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
<meta property="og:title" content={SITE_TITLE} /> <link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
<meta property="og:url" content={SITE_URL} /> <link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
<meta name="description" content={SITE_DESCRIPTION} /> <link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<link rel="apple-touch-icon" sizes="180x180" href={`${prefix}favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${prefix}favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${prefix}favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${prefix}site.webmanifest.json`} />
<link rel="shortcut icon" href={`${prefix}favicon/favicon.ico`} />
</head> </head>
<body className={`antialiased`}> <body className={`antialiased`}>
<StoreProvider {...pageProps}> <AppProvider initialState={{ instance: instanceDetails }}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem> {instanceDetails ? (
<AppWrapper>{children}</AppWrapper> <>{instanceDetails?.instance?.is_setup_done ? <>{children}</> : <InstanceNotReady />}</>
</ThemeProvider> ) : (
</StoreProvider> <InstanceFailureView />
)}
</AppProvider>
</body> </body>
</html> </html>
); );
}; }
export default RootLayout;

View File

@ -1,26 +1,11 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "@/components/login"; import { InstanceSignInForm } from "@/components/login";
// lib // layouts
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers"; import { DefaultLayout } from "@/layouts/default-layout";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
const LoginPage = () => ( export default async function LoginPage() {
<> return (
<PageHeader title="Login - God Mode" /> <DefaultLayout>
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}> <InstanceSignInForm />
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}> </DefaultLayout>
<DefaultLayout> );
<InstanceSignInForm /> }
</DefaultLayout>
</AuthWrapper>
</InstanceWrapper>
</>
);
export default LoginPage;

View File

@ -2,17 +2,17 @@
import { FC, useEffect, useMemo, useState } from "react"; import { FC, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
// services // icons
import { AuthService } from "@/services/auth.service"; import { Eye, EyeOff } from "lucide-react";
// ui // ui
import { Button, Checkbox, Input, Spinner } from "@plane/ui"; import { Button, Checkbox, Input, Spinner } from "@plane/ui";
// components // components
import { Banner, PasswordStrengthMeter } from "components/common"; import { Banner, PasswordStrengthMeter } from "@/components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import { getPasswordStrength } from "@/helpers/password.helper";
// services
import { AuthService } from "@/services/auth.service";
// service initialization // service initialization
const authService = new AuthService(); const authService = new AuthService();
@ -154,7 +154,7 @@ export const InstanceSignUpForm: FC = (props) => {
First name <span className="text-red-500">*</span> First name <span className="text-red-500">*</span>
</label> </label>
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="first_name" id="first_name"
name="first_name" name="first_name"
type="text" type="text"
@ -167,10 +167,10 @@ export const InstanceSignUpForm: FC = (props) => {
</div> </div>
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name"> <label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
Last name Last name <span className="text-red-500">*</span>
</label> </label>
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="last_name" id="last_name"
name="last_name" name="last_name"
type="text" type="text"
@ -187,7 +187,7 @@ export const InstanceSignUpForm: FC = (props) => {
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</label> </label>
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="email" id="email"
name="email" name="email"
type="email" type="email"
@ -204,10 +204,10 @@ export const InstanceSignUpForm: FC = (props) => {
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="company_name"> <label className="text-sm text-onboarding-text-300 font-medium" htmlFor="company_name">
Company name Company name <span className="text-red-500">*</span>
</label> </label>
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="company_name" id="company_name"
name="company_name" name="company_name"
type="text" type="text"
@ -224,7 +224,7 @@ export const InstanceSignUpForm: FC = (props) => {
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="password" id="password"
name="password" name="password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}
@ -239,6 +239,7 @@ export const InstanceSignUpForm: FC = (props) => {
{showPassword ? ( {showPassword ? (
<button <button
type="button" type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)} onClick={() => setShowPassword(false)}
> >
@ -247,6 +248,7 @@ export const InstanceSignUpForm: FC = (props) => {
) : ( ) : (
<button <button
type="button" type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)} onClick={() => setShowPassword(true)}
> >
@ -262,7 +264,7 @@ export const InstanceSignUpForm: FC = (props) => {
<div className="w-full space-y-1"> <div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password"> <label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password Confirm password <span className="text-red-500">*</span>
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
@ -278,6 +280,7 @@ export const InstanceSignUpForm: FC = (props) => {
{showPassword ? ( {showPassword ? (
<button <button
type="button" type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)} onClick={() => setShowPassword(false)}
> >
@ -286,6 +289,7 @@ export const InstanceSignUpForm: FC = (props) => {
) : ( ) : (
<button <button
type="button" type="button"
tabIndex={-1}
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)} onClick={() => setShowPassword(true)}
> >
@ -314,7 +318,13 @@ export const InstanceSignUpForm: FC = (props) => {
> >
Allow Plane to anonymously collect usage events. Allow Plane to anonymously collect usage events.
</label> </label>
<a href="https://docs.plane.so/telemetry" className="text-sm font-medium text-blue-500 hover:text-blue-600"> <a
tabIndex={-1}
href="https://docs.plane.so/telemetry"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-blue-500 hover:text-blue-600"
>
See More See More
</a> </a>
</div> </div>

View File

@ -1,19 +1,23 @@
"use client"; "use client";
import { ReactNode } from "react"; import { ReactNode } from "react";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers // helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers"; import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
interface SetupLayoutProps { interface SetupLayoutProps {
children: ReactNode; children: ReactNode;
params: any;
} }
const SetupLayout = ({ children }: SetupLayoutProps) => ( export default function SetupLayout(props: SetupLayoutProps) {
<InstanceWrapper pageType={EInstancePageType.PRE_SETUP}> const { children, params } = props;
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper> const { error_code } = params;
</InstanceWrapper> console.log("error_code", error_code);
); return (
<InstanceWrapper pageType={EInstancePageType.PRE_SETUP}>
export default SetupLayout; <AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper>
</InstanceWrapper>
);
}

View File

@ -1,16 +1,19 @@
import { Metadata } from "next";
// layouts // layouts
import { DefaultLayout } from "@/layouts"; import { DefaultLayout } from "@/layouts/default-layout";
// components // components
import { PageHeader } from "@/components/core";
import { InstanceSignUpForm } from "./components"; import { InstanceSignUpForm } from "./components";
const SetupPage = () => ( export const metadata: Metadata = {
<> title: "Setup - God Mode",
<PageHeader title="Setup - God Mode" /> };
<DefaultLayout>
<InstanceSignUpForm />
</DefaultLayout>
</>
);
export default SetupPage; export default function SetupPage() {
return (
<>
<DefaultLayout>
<InstanceSignUpForm />
</DefaultLayout>
</>
);
}

View File

@ -1,12 +1,14 @@
"use client"; "use client";
import { FC, useState, useRef } from "react"; import { FC, useState, useRef } from "react";
import { Transition } from "@headlessui/react"; import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
// hooks // hooks
import { useTheme } from "@/hooks"; import { WEB_BASE_URL } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// assets // assets
import packageJson from "package.json"; import packageJson from "package.json";
@ -28,7 +30,7 @@ const helpOptions = [
}, },
]; ];
export const HelpSection: FC = () => { export const HelpSection: FC = observer(() => {
// states // states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store // store
@ -36,7 +38,7 @@ export const HelpSection: FC = () => {
// refs // refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null); const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
return ( return (
<div <div
@ -128,4 +130,4 @@ export const HelpSection: FC = () => {
</div> </div>
</div> </div>
); );
}; });

View File

@ -3,10 +3,10 @@
import { FC, useEffect, useRef } from "react"; import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useTheme } from "@/hooks"; import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
import { useTheme } from "@/hooks/store";
import useOutsideClickDetector from "hooks/use-outside-click-detector"; import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components // components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
export interface IInstanceSidebar {} export interface IInstanceSidebar {}

View File

@ -1,15 +1,15 @@
"use client"; "use client";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { useTheme as useNextTheme } from "next-themes";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useTheme as useNextTheme } from "next-themes";
import { LogOut, UserCog2, Palette } from "lucide-react"; import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react"; import { Menu, Transition } from "@headlessui/react";
import { Avatar } from "@plane/ui"; import { Avatar } from "@plane/ui";
// hooks // hooks
import { useTheme, useUser } from "@/hooks";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper"; import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { useTheme, useUser } from "@/hooks/store";
// helpers
// services // services
import { AuthService } from "@/services"; import { AuthService } from "@/services";

View File

@ -3,9 +3,9 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
// hooks // hooks
import { useTheme } from "@/hooks";
// icons
import { Menu } from "lucide-react"; import { Menu } from "lucide-react";
import { useTheme } from "@/hooks/store";
// icons
export const SidebarHamburgerToggle: FC = observer(() => { export const SidebarHamburgerToggle: FC = observer(() => {
const { isSidebarCollapsed, toggleSidebar } = useTheme(); const { isSidebarCollapsed, toggleSidebar } = useTheme();

View File

@ -1,14 +1,14 @@
"use client"; "use client";
import { observer } from "mobx-react-lite";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { observer } from "mobx-react-lite";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react"; import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Tooltip } from "@plane/ui"; import { Tooltip } from "@plane/ui";
// hooks // hooks
import { useTheme } from "@/hooks";
// helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { useTheme } from "@/hooks/store";
// helpers
const INSTANCE_ADMIN_LINKS = [ const INSTANCE_ADMIN_LINKS = [
{ {

View File

@ -1,16 +1,16 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react-lite";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
// mobx // mobx
import { observer } from "mobx-react-lite";
// ui // ui
import { Settings } from "lucide-react"; import { Settings } from "lucide-react";
// icons // icons
import { Breadcrumbs } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "components/common";
import { SidebarHamburgerToggle } from "@/components/admin-sidebar"; import { SidebarHamburgerToggle } from "@/components/admin-sidebar";
import { BreadcrumbLink } from "components/common";
export const InstanceHeader: FC = observer(() => { export const InstanceHeader: FC = observer(() => {
const pathName = usePathname(); const pathName = usePathname();

View File

@ -3,9 +3,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Controller, Control } from "react-hook-form"; import { Controller, Control } from "react-hook-form";
// ui // ui
import { Eye, EyeOff } from "lucide-react";
import { Input } from "@plane/ui"; import { Input } from "@plane/ui";
// icons // icons
import { Eye, EyeOff } from "lucide-react";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
@ -62,6 +62,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
{type === "password" && {type === "password" &&
(showPassword ? ( (showPassword ? (
<button <button
tabIndex={-1}
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)} onClick={() => setShowPassword(false)}
> >
@ -69,6 +70,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
</button> </button>
) : ( ) : (
<button <button
tabIndex={-1}
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400" className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)} onClick={() => setShowPassword(true)}
> >

View File

@ -2,9 +2,9 @@
import React from "react"; import React from "react";
// ui // ui
import { Copy } from "lucide-react";
import { Button, TOAST_TYPE, setToast } from "@plane/ui"; import { Button, TOAST_TYPE, setToast } from "@plane/ui";
// icons // icons
import { Copy } from "lucide-react";
type Props = { type Props = {
label: string; label: string;
@ -40,7 +40,7 @@ export const CopyField: React.FC<Props> = (props) => {
<p className="text-sm font-medium">{url}</p> <p className="text-sm font-medium">{url}</p>
<Copy size={18} color="#B9B9B9" /> <Copy size={18} color="#B9B9B9" />
</Button> </Button>
<p className="text-xs text-custom-text-400">{description}</p> <div className="text-xs text-custom-text-400">{description}</div>
</div> </div>
); );
}; };

View File

@ -0,0 +1,46 @@
import React from "react";
import Image from "next/image";
import { Button } from "@plane/ui";
type Props = {
title: string;
description?: React.ReactNode;
image?: any;
primaryButton?: {
icon?: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
disabled?: boolean;
};
export const EmptyState: React.FC<Props> = ({
title,
description,
image,
primaryButton,
secondaryButton,
disabled = false,
}) => (
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</Button>
)}
{secondaryButton}
</div>
</div>
</div>
);

View File

@ -4,3 +4,4 @@ export * from "./controller-input";
export * from "./copy-field"; export * from "./copy-field";
export * from "./password-strength-meter"; export * from "./password-strength-meter";
export * from "./banner"; export * from "./banner";
export * from "./empty-state";

View File

@ -1,10 +1,10 @@
"use client"; "use client";
// helpers // helpers
import { CircleCheck } from "lucide-react";
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper"; import { getPasswordStrength } from "@/helpers/password.helper";
// icons // icons
import { CircleCheck } from "lucide-react";
type Props = { type Props = {
password: string; password: string;

View File

@ -1 +1,2 @@
export * from "./instance-not-ready"; export * from "./instance-not-ready";
export * from "./instance-failure-view";

View File

@ -0,0 +1,42 @@
"use client";
import { FC } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
import { Button } from "@plane/ui";
// assets
import InstanceFailureDarkImage from "@/public/instance/instance-failure-dark.svg";
import InstanceFailureImage from "@/public/instance/instance-failure.svg";
type InstanceFailureViewProps = {
// mutate: () => void;
};
export const InstanceFailureView: FC<InstanceFailureViewProps> = () => {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
const handleRetry = () => {
window.location.reload();
};
return (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center mt-10">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<Image src={instanceImage} alt="Plane Logo" />
<h3 className="font-medium text-2xl text-white ">Unable to fetch instance details.</h3>
<p className="font-medium text-base text-center">
We were unable to fetch the details of the instance. <br />
Fret not, it might just be a connectivity issue.
</p>
</div>
<div className="flex justify-center">
<Button size="md" onClick={handleRetry}>
Retry
</Button>
</div>
</div>
</div>
);
};

View File

@ -1,8 +1,8 @@
"use client"; "use client";
import { FC } from "react"; import { FC } from "react";
import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// assets // assets
import PlaneTakeOffImage from "@/public/images/plane-takeoff.png"; import PlaneTakeOffImage from "@/public/images/plane-takeoff.png";

View File

@ -3,15 +3,15 @@
import { FC, useEffect, useMemo, useState } from "react"; import { FC, useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation"; import { useSearchParams } from "next/navigation";
// services // services
import { AuthService } from "@/services/auth.service"; import { Eye, EyeOff } from "lucide-react";
// ui
import { Button, Input, Spinner } from "@plane/ui"; import { Button, Input, Spinner } from "@plane/ui";
// components // components
import { Banner } from "components/common"; import { Banner } from "@/components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers // helpers
import { API_BASE_URL } from "@/helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { AuthService } from "@/services/auth.service";
// ui
// icons
// service initialization // service initialization
const authService = new AuthService(); const authService = new AuthService();
@ -57,6 +57,8 @@ export const InstanceSignInForm: FC = (props) => {
const handleFormChange = (key: keyof TFormData, value: string | boolean) => const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value })); setFormData((prev) => ({ ...prev, [key]: value }));
console.log("csrfToken", csrfToken);
useEffect(() => { useEffect(() => {
if (csrfToken === undefined) if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
@ -119,7 +121,7 @@ export const InstanceSignInForm: FC = (props) => {
Email <span className="text-red-500">*</span> Email <span className="text-red-500">*</span>
</label> </label>
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="email" id="email"
name="email" name="email"
type="email" type="email"
@ -137,7 +139,7 @@ export const InstanceSignInForm: FC = (props) => {
</label> </label>
<div className="relative"> <div className="relative">
<Input <Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400" className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 placeholder:text-onboarding-text-400"
id="password" id="password"
name="password" name="password"
type={showPassword ? "text" : "password"} type={showPassword ? "text" : "password"}

View File

@ -8,18 +8,20 @@ import { useTheme as nextUseTheme } from "next-themes";
import { Button, getButtonStyling } from "@plane/ui"; import { Button, getButtonStyling } from "@plane/ui";
// helpers // helpers
import { resolveGeneralTheme } from "helpers/common.helper"; import { resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
// icons // icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg"; import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg"; import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
import { useTheme } from "@/hooks";
export const NewUserPopup: React.FC = observer(() => { export const NewUserPopup: React.FC = observer(() => {
// hooks // hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme(); const { isNewUserPopup, toggleNewUserPopup } = useTheme();
const { instance } = useInstance();
// theme // theme
const { resolvedTheme } = nextUseTheme(); const { resolvedTheme } = nextUseTheme();
const redirectionLink = `${process.env.NEXT_PUBLIC_APP_URL ? `${process.env.NEXT_PUBLIC_APP_URL}/create-workspace` : `/god-mode/`}`; const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
if (!isNewUserPopup) return <></>; if (!isNewUserPopup) return <></>;
return ( return (

View File

@ -1,10 +1,12 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import Link from "next/link";
export enum EPageTypes { export enum EPageTypes {
"PUBLIC" = "PUBLIC", PUBLIC = "PUBLIC",
"NON_AUTHENTICATED" = "NON_AUTHENTICATED", NON_AUTHENTICATED = "NON_AUTHENTICATED",
"ONBOARDING" = "ONBOARDING", SET_PASSWORD = "SET_PASSWORD",
"AUTHENTICATED" = "AUTHENTICATED", ONBOARDING = "ONBOARDING",
AUTHENTICATED = "AUTHENTICATED",
} }
export enum EAuthModes { export enum EAuthModes {
@ -18,28 +20,26 @@ export enum EAuthSteps {
UNIQUE_CODE = "UNIQUE_CODE", UNIQUE_CODE = "UNIQUE_CODE",
} }
export enum EAuthenticationErrorCodes {
INSTANCE_NOT_CONFIGURED = "5000",
// Admin
ADMIN_ALREADY_EXIST = "5029",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5030",
INVALID_ADMIN_EMAIL = "5031",
INVALID_ADMIN_PASSWORD = "5032",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5033",
ADMIN_AUTHENTICATION_FAILED = "5034",
ADMIN_USER_ALREADY_EXIST = "5035",
ADMIN_USER_DOES_NOT_EXIST = "5036",
}
export enum EErrorAlertType { export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT", BANNER_ALERT = "BANNER_ALERT",
TOAST_ALERT = "TOAST_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME", INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL", INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD", INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE", INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
} }
export enum EAuthenticationErrorCodes {
// Admin
ADMIN_ALREADY_EXIST = "5150",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5155",
INVALID_ADMIN_EMAIL = "5160",
INVALID_ADMIN_PASSWORD = "5165",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5170",
ADMIN_AUTHENTICATION_FAILED = "5175",
ADMIN_USER_ALREADY_EXIST = "5180",
ADMIN_USER_DOES_NOT_EXIST = "5185",
}
export type TAuthErrorInfo = { export type TAuthErrorInfo = {
type: EErrorAlertType; type: EErrorAlertType;
code: EAuthenticationErrorCodes; code: EAuthenticationErrorCodes;
@ -50,41 +50,54 @@ export type TAuthErrorInfo = {
const errorCodeMessages: { const errorCodeMessages: {
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode }; [key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = { } = {
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: { // admin
title: "Instance not configured",
message: () => "Please contact your administrator to configure the instance.",
},
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: { [EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: "Admin already exists", title: `Admin already exists`,
message: () => "Admin already exists. Please sign in.", message: () => `Admin already exists. Please try again.`,
}, },
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: { [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: "Required", title: `Email, password and first name required`,
message: () => "Please enter email, password and first name.", message: () => `Email, password and first name required. Please try again.`,
}, },
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: { [EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: "Invalid email", title: `Invalid admin email`,
message: () => "Please enter a valid email.", message: () => `Invalid admin email. Please try again.`,
}, },
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: { [EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: "Invalid password", title: `Invalid admin password`,
message: () => "Password must be at least 8 characters long.", message: () => `Invalid admin password. Please try again.`,
}, },
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: { [EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: "Required", title: `Email and password required`,
message: () => "Please enter email and password.", message: () => `Email and password required. Please try again.`,
}, },
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: { [EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: "Authentication failed", title: `Authentication failed`,
message: () => "Please check your email and password and try again.", message: () => `Authentication failed. Please try again.`,
}, },
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: { [EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: "User already exists", title: `Admin user already exists`,
message: () => "User already exists. Please sign in.", message: () => (
<div>
Admin user already exists.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
}, },
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: { [EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: "User does not exist", title: `Admin user does not exist`,
message: () => "User does not exist. Please sign up.", message: () => (
<div>
Admin user does not exist.&nbsp;
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
Sign In
</Link>
&nbsp;now.
</div>
),
}, },
}; };
@ -92,28 +105,16 @@ export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes, errorCode: EAuthenticationErrorCodes,
email?: string | undefined email?: string | undefined
): TAuthErrorInfo | undefined => { ): TAuthErrorInfo | undefined => {
const toastAlertErrorCodes = [ const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST, EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL, EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD, EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
];
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD, EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST, EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST, EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
]; ];
if (toastAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.TOAST_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
if (bannerAlertErrorCodes.includes(errorCode)) if (bannerAlertErrorCodes.includes(errorCode))
return { return {
type: EErrorAlertType.BANNER_ALERT, type: EErrorAlertType.BANNER_ALERT,

View File

@ -1,7 +1,16 @@
import { clsx, type ClassValue } from "clsx"; import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : ""; export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const ASSET_PREFIX = ADMIN_BASE_PATH;
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs)); export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));

View File

@ -1,6 +0,0 @@
export * from "./use-outside-click-detector";
// store-hooks
export * from "./store/use-theme";
export * from "./store/use-instance";
export * from "./store/use-user";

View File

@ -0,0 +1,3 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";

View File

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/lib/store-context"; import { StoreContext } from "@/lib/app-providers";
import { IInstanceStore } from "@/store/instance.store"; import { IInstanceStore } from "@/store/instance.store";
export const useInstance = (): IInstanceStore => { export const useInstance = (): IInstanceStore => {

View File

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/lib/store-context"; import { StoreContext } from "@/lib/app-providers";
import { IThemeStore } from "@/store/theme.store"; import { IThemeStore } from "@/store/theme.store";
export const useTheme = (): IThemeStore => { export const useTheme = (): IThemeStore => {

View File

@ -1,6 +1,6 @@
import { useContext } from "react"; import { useContext } from "react";
// store // store
import { StoreContext } from "@/lib/store-context"; import { StoreContext } from "@/lib/app-providers";
import { IUserStore } from "@/store/user.store"; import { IUserStore } from "@/store/user.store";
export const useUser = (): IUserStore => { export const useUser = (): IUserStore => {

View File

@ -1,15 +1,48 @@
import { FC, ReactNode } from "react"; "use client";
import { FC, ReactNode, useEffect } from "react";
import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation";
import useSWR from "swr";
// ui
import { Spinner } from "@plane/ui";
// components // components
import { InstanceSidebar } from "@/components/admin-sidebar"; import { InstanceSidebar } from "@/components/admin-sidebar";
import { InstanceHeader } from "@/components/auth-header"; import { InstanceHeader } from "@/components/auth-header";
import { NewUserPopup } from "@/components/new-user-popup"; import { NewUserPopup } from "@/components/new-user-popup";
// hooks
import { useInstance, useUser } from "@/hooks/store";
type TAdminLayout = { type TAdminLayout = {
children: ReactNode; children: ReactNode;
}; };
export const AdminLayout: FC<TAdminLayout> = (props) => { export const AdminLayout: FC<TAdminLayout> = observer((props) => {
const { children } = props; const { children } = props;
// router
const router = useRouter();
// hooks
const { fetchInstanceAdmins } = useInstance();
const { fetchCurrentUser, isUserLoggedIn } = useUser();
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins());
useSWR("CURRENT_USER", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
useEffect(() => {
if (isUserLoggedIn === false) {
router.push("/");
}
}, [router, isUserLoggedIn]);
if (isUserLoggedIn === undefined) {
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
}
return ( return (
<div className="relative flex h-screen w-screen overflow-hidden"> <div className="relative flex h-screen w-screen overflow-hidden">
@ -21,4 +54,4 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
<NewUserPopup /> <NewUserPopup />
</div> </div>
); );
}; });

View File

@ -17,6 +17,7 @@ export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { children, withoutBackground = false } = props; const { children, withoutBackground = false } = props;
// hooks // hooks
const { resolvedTheme } = useTheme(); const { resolvedTheme } = useTheme();
const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern;
return ( return (
<div className="relative"> <div className="relative">
@ -29,11 +30,7 @@ export const DefaultLayout: FC<TDefaultLayout> = (props) => {
</div> </div>
{!withoutBackground && ( {!withoutBackground && (
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image src={patternBackground} className="w-screen h-full object-cover" alt="Plane background pattern" />
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen h-full object-cover"
alt="Plane background pattern"
/>
</div> </div>
)} )}
<div className="relative z-10 mb-[110px] flex-grow">{children}</div> <div className="relative z-10 mb-[110px] flex-grow">{children}</div>

View File

@ -1,2 +0,0 @@
export * from "./default-layout";
export * from "./admin-layout";

View File

@ -0,0 +1,44 @@
"use client";
import { ReactNode, createContext } from "react";
import { ThemeProvider } from "next-themes";
// ui
import { AppWrapper } from "@/lib/wrappers";
// store
import { RootStore } from "@/store/root.store";
let rootStore = new RootStore();
export const StoreContext = createContext(rootStore);
function initializeStore(initialData = {}) {
const singletonRootStore = rootStore ?? new RootStore();
// If your page has Next.js data fetching methods that use a Mobx store, it will
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
if (initialData) {
console.log("initialState", initialData);
singletonRootStore.hydrate(initialData);
}
// For SSG and SSR always create a new store
if (typeof window === "undefined") return singletonRootStore;
// Create the store once in the client
if (!rootStore) rootStore = singletonRootStore;
return singletonRootStore;
}
export type AppProviderProps = {
children: ReactNode;
initialState: any;
};
export const AppProvider = ({ children, initialState = {} }: AppProviderProps) => {
const store = initializeStore(initialState);
return (
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
<StoreContext.Provider value={store}>
<AppWrapper>{children}</AppWrapper>
</StoreContext.Provider>
</ThemeProvider>
);
};

View File

@ -1,21 +0,0 @@
"use client";
import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root-store";
export let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
const initializeStore = () => {
const newRootStore = rootStore ?? new RootStore();
if (typeof window === "undefined") return newRootStore;
if (!rootStore) rootStore = newRootStore;
return newRootStore;
};
export const StoreProvider = ({ children }: { children: ReactElement }) => {
const store = initializeStore();
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};

View File

@ -3,14 +3,14 @@
import { FC, ReactNode, useEffect, Suspense } from "react"; import { FC, ReactNode, useEffect, Suspense } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { SWRConfig } from "swr"; import { SWRConfig } from "swr";
// hooks
import { useTheme, useUser } from "@/hooks";
// ui // ui
import { Toast } from "@plane/ui"; import { Toast } from "@plane/ui";
// constants // constants
import { SWR_CONFIG } from "constants/swr-config"; import { SWR_CONFIG } from "@/constants/swr-config";
// helpers // helpers
import { resolveGeneralTheme } from "helpers/common.helper"; import { resolveGeneralTheme } from "@/helpers/common.helper";
// hooks
import { useTheme, useUser } from "@/hooks/store";
interface IAppWrapper { interface IAppWrapper {
children: ReactNode; children: ReactNode;

View File

@ -1,14 +1,14 @@
"use client"; "use client";
import { FC, ReactNode } from "react"; import { FC, ReactNode } from "react";
import { useRouter } from "next/navigation";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useRouter } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// hooks // hooks
import { useInstance, useUser } from "@/hooks";
// helpers
import { EAuthenticationPageType } from "@/helpers"; import { EAuthenticationPageType } from "@/helpers";
import { useInstance, useUser } from "@/hooks/store";
// helpers
export interface IAuthWrapper { export interface IAuthWrapper {
children: ReactNode; children: ReactNode;

View File

@ -1,18 +1,19 @@
"use client"; "use client";
import { FC, ReactNode } from "react"; import { FC, ReactNode } from "react";
import { redirect, useSearchParams } from "next/navigation";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { redirect, useSearchParams } from "next/navigation";
import useSWR from "swr"; import useSWR from "swr";
import { Spinner } from "@plane/ui"; import { Spinner } from "@plane/ui";
// layouts
import { DefaultLayout } from "@/layouts";
// components // components
import { EmptyState } from "@/components/common";
import { InstanceNotReady } from "@/components/instance"; import { InstanceNotReady } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks";
// helpers // helpers
import { EInstancePageType } from "@/helpers"; import { EInstancePageType } from "@/helpers";
// hooks
import { useInstance } from "@/hooks/store";
// layouts
import { DefaultLayout } from "@/layouts/default-layout";
type TInstanceWrapper = { type TInstanceWrapper = {
children: ReactNode; children: ReactNode;
@ -28,6 +29,9 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), { const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false, revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
errorRetryCount: 0,
}); });
if (isSWRLoading || isLoading) if (isSWRLoading || isLoading)
@ -37,6 +41,15 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
</div> </div>
); );
if (!instance) {
return (
<EmptyState
title="Your instance wasn't configured successfully."
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
/>
);
}
if (instance?.instance?.is_setup_done === false && authEnabled === "1") if (instance?.instance?.is_setup_done === false && authEnabled === "1")
return ( return (
<DefaultLayout withoutBackground> <DefaultLayout withoutBackground>

View File

@ -7,7 +7,7 @@ const nextConfig = {
images: { images: {
unoptimized: true, unoptimized: true,
}, },
basePath: "/god-mode", basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
}; };
module.exports = nextConfig; module.exports = nextConfig;

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "turbo run develop", "dev": "turbo run develop",
"develop": "next dev --port 3333", "develop": "next dev --port 3001",
"build": "next build", "build": "next build",
"preview": "next build && next start", "preview": "next build && next start",
"start": "next start", "start": "next start",
@ -14,6 +14,7 @@
"@headlessui/react": "^1.7.19", "@headlessui/react": "^1.7.19",
"@plane/types": "*", "@plane/types": "*",
"@plane/ui": "*", "@plane/ui": "*",
"@plane/constants": "*",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0", "@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
@ -25,7 +26,7 @@
"mobx-react-lite": "^4.0.5", "mobx-react-lite": "^4.0.5",
"next": "^14.2.3", "next": "^14.2.3",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"postcss": "8.4.23", "postcss": "^8.4.38",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-hook-form": "^7.51.0", "react-hook-form": "^7.51.0",

View File

@ -0,0 +1,40 @@
<svg width="210" height="206" viewBox="0 0 210 206" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="107.5" cy="103" r="102.5" fill="#24252C"/>
<path d="M140.625 162.125V148.875C138.868 148.875 137.183 148.177 135.94 146.935C134.698 145.692 134 144.007 134 142.25V135.625C134 132.111 135.396 128.741 137.881 126.256C140.366 123.771 143.736 122.375 147.25 122.375H160.5C164.014 122.375 167.384 123.771 169.869 126.256C172.354 128.741 173.75 132.111 173.75 135.625V142.25C173.75 144.007 173.052 145.692 171.81 146.935C170.567 148.177 168.882 148.875 167.125 148.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M153.875 122.375V66.0625C153.875 59.9128 151.432 54.015 147.084 49.6665C142.735 45.318 136.837 42.875 130.687 42.875C124.538 42.875 118.64 45.318 114.291 49.6665C109.943 54.015 107.5 59.9128 107.5 66.0625M107.5 138.937C107.5 145.087 105.057 150.985 100.709 155.334C96.36 159.682 90.4622 162.125 84.3125 162.125C78.1628 162.125 72.265 159.682 67.9165 155.334C63.568 150.985 61.125 145.087 61.125 138.937V82.625" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M167.125 162.125V148.875H140.625" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M47.875 56.125H74.375V42.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74.375 56.125C76.1321 56.125 77.8172 56.823 79.0596 58.0654C80.302 59.3078 81 60.9929 81 62.75V69.375C81 72.8891 79.604 76.2593 77.1192 78.7442C74.6343 81.229 71.2641 82.625 67.75 82.625H54.5C50.9859 82.625 47.6157 81.229 45.1308 78.7442C42.646 76.2593 41.25 72.8891 41.25 69.375V62.75C41.25 60.9929 41.948 59.3078 43.1904 58.0654C44.4328 56.823 46.1179 56.125 47.875 56.125V42.875" stroke="#454961" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<g filter="url(#filter0_ddd_11437_265561)">
<circle cx="107.911" cy="102.911" r="23.7938" fill="#3A5BC7"/>
<path d="M114.051 96.7712L101.771 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M101.771 96.7712L114.051 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<filter id="filter0_ddd_11437_265561" x="76.1172" y="74.1177" width="63.5879" height="64.5876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_11437_265561"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0 0 0 0.051 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_11437_265561"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect2_dropShadow_11437_265561"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="6"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.055 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_11437_265561" result="effect2_dropShadow_11437_265561"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="8" operator="erode" in="SourceAlpha" result="effect3_dropShadow_11437_265561"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="8"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.078 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_11437_265561" result="effect3_dropShadow_11437_265561"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_11437_265561" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,40 @@
<svg width="210" height="206" viewBox="0 0 210 206" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="107.5" cy="103" r="102.5" fill="#F3F6FF"/>
<path d="M140.625 162.125V148.875C138.868 148.875 137.183 148.177 135.94 146.935C134.698 145.692 134 144.007 134 142.25V135.625C134 132.111 135.396 128.741 137.881 126.256C140.366 123.771 143.736 122.375 147.25 122.375H160.5C164.014 122.375 167.384 123.771 169.869 126.256C172.354 128.741 173.75 132.111 173.75 135.625V142.25C173.75 144.007 173.052 145.692 171.81 146.935C170.567 148.177 168.882 148.875 167.125 148.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M153.875 122.375V66.0625C153.875 59.9128 151.432 54.015 147.084 49.6665C142.735 45.318 136.837 42.875 130.687 42.875C124.538 42.875 118.64 45.318 114.291 49.6665C109.943 54.015 107.5 59.9128 107.5 66.0625M107.5 138.937C107.5 145.087 105.057 150.985 100.709 155.334C96.36 159.682 90.4622 162.125 84.3125 162.125C78.1628 162.125 72.265 159.682 67.9165 155.334C63.568 150.985 61.125 145.087 61.125 138.937V82.625" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M167.125 162.125V148.875H140.625" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M47.875 56.125H74.375V42.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74.375 56.125C76.1321 56.125 77.8172 56.823 79.0596 58.0654C80.302 59.3078 81 60.9929 81 62.75V69.375C81 72.8891 79.604 76.2593 77.1192 78.7442C74.6343 81.229 71.2641 82.625 67.75 82.625H54.5C50.9859 82.625 47.6157 81.229 45.1308 78.7442C42.646 76.2593 41.25 72.8891 41.25 69.375V62.75C41.25 60.9929 41.948 59.3078 43.1904 58.0654C44.4328 56.823 46.1179 56.125 47.875 56.125V42.875" stroke="#3E63DD" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<g filter="url(#filter0_ddd_11424_265422)">
<circle cx="107.911" cy="102.911" r="23.7938" fill="#3A5BC7"/>
<path d="M114.051 96.7712L101.771 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M101.771 96.7712L114.051 109.052" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<filter id="filter0_ddd_11424_265422" x="76.1172" y="74.1177" width="63.5879" height="64.5876" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="2" operator="erode" in="SourceAlpha" result="effect1_dropShadow_11424_265422"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.24 0 0 0 0.051 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_11424_265422"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect2_dropShadow_11424_265422"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="6"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.055 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_11424_265422" result="effect2_dropShadow_11424_265422"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="8" operator="erode" in="SourceAlpha" result="effect3_dropShadow_11424_265422"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="8"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.078 0"/>
<feBlend mode="normal" in2="effect2_dropShadow_11424_265422" result="effect3_dropShadow_11424_265422"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect3_dropShadow_11424_265422" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,6 +1,6 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// store // store
import { rootStore } from "@/lib/store-context"; // import { rootStore } from "@/lib/store-context";
export abstract class APIService { export abstract class APIService {
protected baseURL: string; protected baseURL: string;
@ -17,14 +17,14 @@ export abstract class APIService {
} }
private setupInterceptors() { private setupInterceptors() {
this.axiosInstance.interceptors.response.use( // this.axiosInstance.interceptors.response.use(
(response) => response, // (response) => response,
(error) => { // (error) => {
const store = rootStore; // const store = rootStore;
if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset(); // if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
return Promise.reject(error); // return Promise.reject(error);
} // }
); // );
} }
get<ResponseType>(url: string, params = {}): Promise<AxiosResponse<ResponseType>> { get<ResponseType>(url: string, params = {}): Promise<AxiosResponse<ResponseType>> {

View File

@ -1,7 +1,7 @@
// services
import { APIService } from "services/api.service";
// helpers // helpers
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "helpers/common.helper";
// services
import { APIService } from "services/api.service";
type TCsrfTokenResponse = { type TCsrfTokenResponse = {
csrf_token: string; csrf_token: string;

View File

@ -1,8 +1,8 @@
import { APIService } from "services/api.service";
// types // types
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types"; import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
// helpers // helpers
import { API_BASE_URL } from "helpers/common.helper"; import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
export class InstanceService extends APIService { export class InstanceService extends APIService {
constructor() { constructor() {
@ -13,6 +13,7 @@ export class InstanceService extends APIService {
return this.get<IInstance>("/api/instances/") return this.get<IInstance>("/api/instances/")
.then((response) => response.data) .then((response) => response.data)
.catch((error) => { .catch((error) => {
console.log("error", error);
throw error; throw error;
}); });
} }

View File

@ -1,15 +1,25 @@
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// services // services
import { APIService } from "services/api.service"; import { APIService } from "services/api.service";
// types // types
import type { IUser } from "@plane/types"; import type { IUser } from "@plane/types";
// helpers
import { API_BASE_URL } from "helpers/common.helper"; interface IUserSession extends IUser {
isAuthenticated: boolean;
}
export class UserService extends APIService { export class UserService extends APIService {
constructor() { constructor() {
super(API_BASE_URL); super(API_BASE_URL);
} }
async authCheck(): Promise<IUserSession> {
return this.get<any>("/api/instances/admins/me/")
.then((response) => ({ ...response?.data, isAuthenticated: true }))
.catch(() => ({ isAuthenticated: false }));
}
async currentUser(): Promise<IUser> { async currentUser(): Promise<IUser> {
return this.get<IUser>("/api/instances/admins/me/") return this.get<IUser>("/api/instances/admins/me/")
.then((response) => response?.data) .then((response) => response?.data)

View File

@ -1,12 +1,12 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
import set from "lodash/set"; import set from "lodash/set";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
import { IInstance, IInstanceAdmin, IInstanceConfiguration, IFormattedInstanceConfiguration } from "@plane/types"; import { IInstance, IInstanceAdmin, IInstanceConfiguration, IFormattedInstanceConfiguration } from "@plane/types";
// helpers // helpers
import { EInstanceStatus, TInstanceStatus } from "@/helpers"; import { EInstanceStatus, TInstanceStatus } from "@/helpers";
// services // services
import { InstanceService } from "@/services/instance.service"; import { InstanceService } from "@/services/instance.service";
// root store // root store
import { RootStore } from "@/store/root-store"; import { RootStore } from "@/store/root.store";
export interface IInstanceStore { export interface IInstanceStore {
// issues // issues
@ -18,11 +18,12 @@ export interface IInstanceStore {
// computed // computed
formattedConfig: IFormattedInstanceConfiguration | undefined; formattedConfig: IFormattedInstanceConfiguration | undefined;
// action // action
hydrate: (data: any) => void;
fetchInstanceInfo: () => Promise<IInstance | undefined>; fetchInstanceInfo: () => Promise<IInstance | undefined>;
updateInstanceInfo: (data: Partial<IInstance["instance"]>) => Promise<IInstance["instance"] | undefined>; updateInstanceInfo: (data: Partial<IInstance["instance"]>) => Promise<IInstance["instance"] | undefined>;
fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>; fetchInstanceAdmins: () => Promise<IInstanceAdmin[] | undefined>;
fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>; fetchInstanceConfigurations: () => Promise<IInstanceConfiguration[] | undefined>;
updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<void>; updateInstanceConfigurations: (data: Partial<IFormattedInstanceConfiguration>) => Promise<IInstanceConfiguration[]>;
} }
export class InstanceStore implements IInstanceStore { export class InstanceStore implements IInstanceStore {
@ -45,6 +46,7 @@ export class InstanceStore implements IInstanceStore {
// computed // computed
formattedConfig: computed, formattedConfig: computed,
// actions // actions
hydrate: action,
fetchInstanceInfo: action, fetchInstanceInfo: action,
fetchInstanceAdmins: action, fetchInstanceAdmins: action,
updateInstanceInfo: action, updateInstanceInfo: action,
@ -55,6 +57,10 @@ export class InstanceStore implements IInstanceStore {
this.instanceService = new InstanceService(); this.instanceService = new InstanceService();
} }
hydrate = (data: any) => {
if (data) this.instance = data;
};
/** /**
* computed value for instance configurations data for forms. * computed value for instance configurations data for forms.
* @returns configurations in the form of {key, value} pair. * @returns configurations in the form of {key, value} pair.
@ -148,13 +154,15 @@ export class InstanceStore implements IInstanceStore {
*/ */
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => { updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
try { try {
await this.instanceService.updateInstanceConfigurations(data).then((response) => { const response = await this.instanceService.updateInstanceConfigurations(data);
runInAction(() => { runInAction(() => {
this.instanceConfigurations = this.instanceConfigurations this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
? [...this.instanceConfigurations, ...response] const item = response.find((item) => item.key === config.key);
: response; if (item) return item;
return config;
}); });
}); });
return response;
} catch (error) { } catch (error) {
console.error("Error updating the instance configurations"); console.error("Error updating the instance configurations");
throw error; throw error;

View File

@ -1,7 +1,7 @@
import { enableStaticRendering } from "mobx-react-lite"; import { enableStaticRendering } from "mobx-react-lite";
// stores // stores
import { IThemeStore, ThemeStore } from "./theme.store";
import { IInstanceStore, InstanceStore } from "./instance.store"; import { IInstanceStore, InstanceStore } from "./instance.store";
import { IThemeStore, ThemeStore } from "./theme.store";
import { IUserStore, UserStore } from "./user.store"; import { IUserStore, UserStore } from "./user.store";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -17,9 +17,14 @@ export class RootStore {
this.user = new UserStore(this); this.user = new UserStore(this);
} }
hydrate(initialData: any) {
this.theme.hydrate(initialData.theme);
this.instance.hydrate(initialData.instance);
this.user.hydrate(initialData.user);
}
resetOnSignOut() { resetOnSignOut() {
localStorage.setItem("theme", "system"); localStorage.setItem("theme", "system");
this.instance = new InstanceStore(this); this.instance = new InstanceStore(this);
this.user = new UserStore(this); this.user = new UserStore(this);
this.theme = new ThemeStore(this); this.theme = new ThemeStore(this);

View File

@ -1,6 +1,6 @@
import { action, observable, makeObservable } from "mobx"; import { action, observable, makeObservable } from "mobx";
// root store // root store
import { RootStore } from "@/store/root-store"; import { RootStore } from "@/store/root.store";
type TTheme = "dark" | "light"; type TTheme = "dark" | "light";
export interface IThemeStore { export interface IThemeStore {
@ -9,6 +9,7 @@ export interface IThemeStore {
theme: string | undefined; theme: string | undefined;
isSidebarCollapsed: boolean | undefined; isSidebarCollapsed: boolean | undefined;
// actions // actions
hydrate: (data: any) => void;
toggleNewUserPopup: () => void; toggleNewUserPopup: () => void;
toggleSidebar: (collapsed: boolean) => void; toggleSidebar: (collapsed: boolean) => void;
setTheme: (currentTheme: TTheme) => void; setTheme: (currentTheme: TTheme) => void;
@ -33,6 +34,10 @@ export class ThemeStore implements IThemeStore {
}); });
} }
hydrate = (data: any) => {
if (data) this.theme = data;
};
/** /**
* @description Toggle the new user popup modal * @description Toggle the new user popup modal
*/ */

View File

@ -3,10 +3,10 @@ import { IUser } from "@plane/types";
// helpers // helpers
import { EUserStatus, TUserStatus } from "@/helpers"; import { EUserStatus, TUserStatus } from "@/helpers";
// services // services
import { AuthService } from "@/services";
import { UserService } from "@/services/user.service"; import { UserService } from "@/services/user.service";
// root store // root store
import { RootStore } from "@/store/root-store"; import { RootStore } from "@/store/root.store";
import { AuthService } from "@/services";
export interface IUserStore { export interface IUserStore {
// observables // observables
@ -15,6 +15,7 @@ export interface IUserStore {
isUserLoggedIn: boolean | undefined; isUserLoggedIn: boolean | undefined;
currentUser: IUser | undefined; currentUser: IUser | undefined;
// fetch actions // fetch actions
hydrate: (data: any) => void;
fetchCurrentUser: () => Promise<IUser>; fetchCurrentUser: () => Promise<IUser>;
reset: () => void; reset: () => void;
signOut: () => void; signOut: () => void;
@ -46,6 +47,10 @@ export class UserStore implements IUserStore {
this.authService = new AuthService(); this.authService = new AuthService();
} }
hydrate = (data: any) => {
if (data) this.currentUser = data;
};
/** /**
* @description Fetches the current user * @description Fetches the current user
* @returns Promise<IUser> * @returns Promise<IUser>

View File

@ -44,3 +44,8 @@ WEB_URL="http://localhost"
# Gunicorn Workers # Gunicorn Workers
GUNICORN_WORKERS=2 GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=

View File

@ -315,7 +315,7 @@ class IssueLinkSerializer(BaseSerializer):
if IssueLink.objects.filter( if IssueLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"),
issue_id=instance.issue_id, issue_id=instance.issue_id,
).exists(): ).exclude(pk=instance.id).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )

View File

@ -1,5 +1,4 @@
# Python imports # Python imports
import zoneinfo import zoneinfo
# Django imports # Django imports

View File

@ -462,7 +462,7 @@ class IssueLinkSerializer(BaseSerializer):
if IssueLink.objects.filter( if IssueLink.objects.filter(
url=validated_data.get("url"), url=validated_data.get("url"),
issue_id=instance.issue_id, issue_id=instance.issue_id,
).exists(): ).exclude(pk=instance.id).exists():
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "URL already exists for this Issue"} {"error": "URL already exists for this Issue"}
) )

View File

@ -11,6 +11,7 @@ from plane.app.views import (
UserEndpoint, UserEndpoint,
UserIssueCompletedGraphEndpoint, UserIssueCompletedGraphEndpoint,
UserWorkspaceDashboardEndpoint, UserWorkspaceDashboardEndpoint,
UserSessionEndpoint,
## End User ## End User
## Workspaces ## Workspaces
UserWorkSpacesEndpoint, UserWorkSpacesEndpoint,
@ -29,6 +30,11 @@ urlpatterns = [
), ),
name="users", name="users",
), ),
path(
"users/session/",
UserSessionEndpoint.as_view(),
name="user-session",
),
path( path(
"users/me/settings/", "users/me/settings/",
UserEndpoint.as_view( UserEndpoint.as_view(

View File

@ -223,4 +223,4 @@ from .error_404 import custom_404_view
from .exporter.base import ExportIssuesEndpoint from .exporter.base import ExportIssuesEndpoint
from .notification.base import MarkAllReadNotificationViewSet from .notification.base import MarkAllReadNotificationViewSet
from .user.base import AccountEndpoint, ProfileEndpoint from .user.base import AccountEndpoint, ProfileEndpoint, UserSessionEndpoint

View File

@ -241,6 +241,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues", "backlog_issues",
"assignee_ids", "assignee_ids",
"status", "status",
"created_by"
) )
if data: if data:
@ -365,6 +366,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues", "backlog_issues",
"assignee_ids", "assignee_ids",
"status", "status",
"created_by",
) )
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
@ -564,6 +566,7 @@ class CycleViewSet(BaseViewSet):
"backlog_issues", "backlog_issues",
"assignee_ids", "assignee_ids",
"status", "status",
"created_by",
) )
.first() .first()
) )

View File

@ -6,6 +6,7 @@ from django.utils import timezone
# Third party imports # Third party imports
from rest_framework import status from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.permissions import AllowAny
# Module imports # Module imports
from plane.app.serializers import ( from plane.app.serializers import (
@ -180,6 +181,25 @@ class UserEndpoint(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class UserSessionEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request):
if request.user.is_authenticated:
user = User.objects.get(pk=request.user.id)
serializer = UserMeSerializer(user)
data = {"is_authenticated": True}
data["user"] = serializer.data
return Response(data, status=status.HTTP_200_OK)
else:
return Response(
{"is_authenticated": False}, status=status.HTTP_200_OK
)
class UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserOnBoardedEndpoint(BaseAPIView):
@invalidate_cache(path="/api/users/me/") @invalidate_cache(path="/api/users/me/")

View File

@ -96,6 +96,7 @@ class WorkSpaceViewSet(BaseViewSet):
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/") @invalidate_cache(path="/api/users/me/workspaces/")
@invalidate_cache(path="/api/instances/", user=False)
def create(self, request): def create(self, request):
try: try:
serializer = WorkSpaceSerializer(data=request.data) serializer = WorkSpaceSerializer(data=request.data)
@ -151,8 +152,12 @@ class WorkSpaceViewSet(BaseViewSet):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
@invalidate_cache(path="/api/workspaces/", user=False) @invalidate_cache(path="/api/workspaces/", user=False)
@invalidate_cache(path="/api/users/me/workspaces/", multiple=True, user=False) @invalidate_cache(
@invalidate_cache(path="/api/users/me/settings/", multiple=True, user=False) path="/api/users/me/workspaces/", multiple=True, user=False
)
@invalidate_cache(
path="/api/users/me/settings/", multiple=True, user=False
)
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs) return super().destroy(request, *args, **kwargs)

View File

@ -100,6 +100,12 @@ class Adapter:
user.save() user.save()
Profile.objects.create(user=user) Profile.objects.create(user=user)
if not user.is_active:
raise AuthenticationException(
AUTHENTICATION_ERROR_CODES["USER_ACCOUNT_DEACTIVATED"],
error_message="USER_ACCOUNT_DEACTIVATED",
)
# Update user details # Update user details
user.last_login_medium = self.provider user.last_login_medium = self.provider
user.last_active = timezone.now() user.last_active = timezone.now()

View File

@ -1,51 +1,57 @@
AUTHENTICATION_ERROR_CODES = { AUTHENTICATION_ERROR_CODES = {
# Global # Global
"INSTANCE_NOT_CONFIGURED": 5000, "INSTANCE_NOT_CONFIGURED": 5000,
"INVALID_EMAIL": 5012, "INVALID_EMAIL": 5005,
"EMAIL_REQUIRED": 5013, "EMAIL_REQUIRED": 5010,
"SIGNUP_DISABLED": 5001, "SIGNUP_DISABLED": 5015,
"MAGIC_LINK_LOGIN_DISABLED": 5016,
"PASSWORD_LOGIN_DISABLED": 5018,
"USER_ACCOUNT_DEACTIVATED": 5019,
# Password strength # Password strength
"INVALID_PASSWORD": 5002, "INVALID_PASSWORD": 5020,
"SMTP_NOT_CONFIGURED": 5007, "SMTP_NOT_CONFIGURED": 5025,
# Sign Up # Sign Up
"USER_ALREADY_EXIST": 5003, "USER_ALREADY_EXIST": 5030,
"AUTHENTICATION_FAILED_SIGN_UP": 5006, "AUTHENTICATION_FAILED_SIGN_UP": 5035,
"REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5015, "REQUIRED_EMAIL_PASSWORD_SIGN_UP": 5040,
"INVALID_EMAIL_SIGN_UP": 5017, "INVALID_EMAIL_SIGN_UP": 5045,
"INVALID_EMAIL_MAGIC_SIGN_UP": 5019, "INVALID_EMAIL_MAGIC_SIGN_UP": 5050,
"MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5023, "MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5055,
# Sign In # Sign In
"USER_DOES_NOT_EXIST": 5004, "USER_DOES_NOT_EXIST": 5060,
"AUTHENTICATION_FAILED_SIGN_IN": 5005, "AUTHENTICATION_FAILED_SIGN_IN": 5065,
"REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5014, "REQUIRED_EMAIL_PASSWORD_SIGN_IN": 5070,
"INVALID_EMAIL_SIGN_IN": 5016, "INVALID_EMAIL_SIGN_IN": 5075,
"INVALID_EMAIL_MAGIC_SIGN_IN": 5018, "INVALID_EMAIL_MAGIC_SIGN_IN": 5080,
"MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5022, "MAGIC_SIGN_IN_EMAIL_CODE_REQUIRED": 5085,
# Both Sign in and Sign up # Both Sign in and Sign up for magic
"INVALID_MAGIC_CODE": 5008, "INVALID_MAGIC_CODE": 5090,
"EXPIRED_MAGIC_CODE": 5009, "EXPIRED_MAGIC_CODE": 5095,
"EMAIL_CODE_ATTEMPT_EXHAUSTED": 5100,
# Oauth # Oauth
"GOOGLE_NOT_CONFIGURED": 5010, "GOOGLE_NOT_CONFIGURED": 5105,
"GITHUB_NOT_CONFIGURED": 5011, "GITHUB_NOT_CONFIGURED": 5110,
"GOOGLE_OAUTH_PROVIDER_ERROR": 5021, "GOOGLE_OAUTH_PROVIDER_ERROR": 5115,
"GITHUB_OAUTH_PROVIDER_ERROR": 5020, "GITHUB_OAUTH_PROVIDER_ERROR": 5120,
# Reset Password # Reset Password
"INVALID_PASSWORD_TOKEN": 5024, "INVALID_PASSWORD_TOKEN": 5125,
"EXPIRED_PASSWORD_TOKEN": 5025, "EXPIRED_PASSWORD_TOKEN": 5130,
# Change password # Change password
"INCORRECT_OLD_PASSWORD": 5026, "INCORRECT_OLD_PASSWORD": 5135,
"INVALID_NEW_PASSWORD": 5027, "MISSING_PASSWORD": 5138,
"INVALID_NEW_PASSWORD": 5140,
# set passowrd # set passowrd
"PASSWORD_ALREADY_SET": 5028, "PASSWORD_ALREADY_SET": 5145,
# Admin # Admin
"ADMIN_ALREADY_EXIST": 5029, "ADMIN_ALREADY_EXIST": 5150,
"REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5030, "REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME": 5155,
"INVALID_ADMIN_EMAIL": 5031, "INVALID_ADMIN_EMAIL": 5160,
"INVALID_ADMIN_PASSWORD": 5032, "INVALID_ADMIN_PASSWORD": 5165,
"REQUIRED_ADMIN_EMAIL_PASSWORD": 5033, "REQUIRED_ADMIN_EMAIL_PASSWORD": 5170,
"ADMIN_AUTHENTICATION_FAILED": 5034, "ADMIN_AUTHENTICATION_FAILED": 5175,
"ADMIN_USER_ALREADY_EXIST": 5035, "ADMIN_USER_ALREADY_EXIST": 5180,
"ADMIN_USER_DOES_NOT_EXIST": 5036, "ADMIN_USER_DOES_NOT_EXIST": 5185,
"ADMIN_USER_DEACTIVATED": 5190,
} }

View File

@ -1,3 +1,6 @@
# Python imports
import os
# Module imports # Module imports
from plane.authentication.adapter.credential import CredentialAdapter from plane.authentication.adapter.credential import CredentialAdapter
from plane.db.models import User from plane.db.models import User
@ -5,6 +8,7 @@ from plane.authentication.adapter.error import (
AUTHENTICATION_ERROR_CODES, AUTHENTICATION_ERROR_CODES,
AuthenticationException, AuthenticationException,
) )
from plane.license.utils.instance_value import get_configuration_value
class EmailProvider(CredentialAdapter): class EmailProvider(CredentialAdapter):
@ -23,6 +27,21 @@ class EmailProvider(CredentialAdapter):
self.code = code self.code = code
self.is_signup = is_signup self.is_signup = is_signup
(ENABLE_EMAIL_PASSWORD,) = get_configuration_value(
[
{
"key": "ENABLE_EMAIL_PASSWORD",
"default": os.environ.get("ENABLE_EMAIL_PASSWORD"),
},
]
)
if ENABLE_EMAIL_PASSWORD == "0":
raise AuthenticationException(
error_code=AUTHENTICATION_ERROR_CODES["ENABLE_EMAIL_PASSWORD"],
error_message="ENABLE_EMAIL_PASSWORD",
)
def set_user_data(self): def set_user_data(self):
if self.is_signup: if self.is_signup:
# Check if the user already exists # Check if the user already exists

Some files were not shown because too many files have changed in this diff Show More