diff --git a/admin/components/admin-sidebar/help-section.tsx b/admin/components/admin-sidebar/help-section.tsx index 8b3f5baeb..84e28c67a 100644 --- a/admin/components/admin-sidebar/help-section.tsx +++ b/admin/components/admin-sidebar/help-section.tsx @@ -7,9 +7,10 @@ import { Transition } from "@headlessui/react"; import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react"; import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui"; // hooks -import { useInstance, useTheme } from "@/hooks/store"; +import { useTheme } from "@/hooks/store"; // assets import packageJson from "package.json"; +import { WEB_BASE_URL } from "@/helpers/common.helper"; const helpOptions = [ { @@ -30,8 +31,6 @@ const helpOptions = [ ]; export const HelpSection: FC = observer(() => { - // hooks - const { instance } = useInstance(); // states const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false); // store @@ -39,7 +38,7 @@ export const HelpSection: FC = observer(() => { // refs const helpOptionsRef = useRef(null); - const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`; + const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace"); return (
void; +}; + +export const InstanceFailureView: FC = (props) => { + const { mutate } = props; + const { resolvedTheme } = useTheme(); + + const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage; + + return ( +
+
+
+ Plane Logo +

Unable to fetch instance details.

+

+ We were unable to fetch the details of the instance.
+ Fret not, it might just be a connectivity issue. +

+
+
+ +
+
+
+ ); +}; diff --git a/space/components/instance/not-ready-view.tsx b/space/components/instance/not-ready-view.tsx index a28fcf3e7..815e0d1fe 100644 --- a/space/components/instance/not-ready-view.tsx +++ b/space/components/instance/not-ready-view.tsx @@ -1,40 +1,33 @@ import { FC } from "react"; import Image from "next/image"; -import { useTheme } from "next-themes"; -// icons -import { UserCog2 } from "lucide-react"; +import Link from "next/link"; // ui -import { getButtonStyling } from "@plane/ui"; +import { Button } from "@plane/ui"; +// helpers +import { ADMIN_BASE_URL, ADMIN_BASE_PATH } from "@/helpers/common.helper"; // images -import instanceNotReady from "public/instance/plane-instance-not-ready.webp"; -import PlaneBlackLogo from "public/plane-logos/black-horizontal-with-blue-logo.svg"; -import PlaneWhiteLogo from "public/plane-logos/white-horizontal-with-blue-logo.svg"; +import PlaneTakeOffImage from "@/public/instance/plane-takeoff.png"; export const InstanceNotReady: FC = () => { - const { resolvedTheme } = useTheme(); - - const planeLogo = resolvedTheme === "dark" ? PlaneWhiteLogo : PlaneBlackLogo; + const GOD_MODE_URL = encodeURI(ADMIN_BASE_URL + ADMIN_BASE_PATH + "/setup/?auth_enabled=0"); return ( -
-
-
-
-
- Plane logo -
-
- Instance not ready -
-
-

Your Plane instance isn{"'"}t ready yet

-

Ask your Instance Admin to complete set-up first.

- - - Get started - -
-
+
+
+
+

Welcome aboard Plane!

+ Plane Logo +

+ Get started by setting up your instance and workspace +

+
+ +
+ + +
diff --git a/space/helpers/common.helper.ts b/space/helpers/common.helper.ts index f39cddc0e..99e04e559 100644 --- a/space/helpers/common.helper.ts +++ b/space/helpers/common.helper.ts @@ -3,6 +3,9 @@ import { twMerge } from "tailwind-merge"; export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || ""; +export const ADMIN_BASE_URL = process.env.NEXT_PUBLIC_ADMIN_BASE_URL || ""; +export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || ""; + export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || ""; export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || ""; diff --git a/space/layouts/project-layout.tsx b/space/layouts/project-layout.tsx index c5946277f..0411bcbcc 100644 --- a/space/layouts/project-layout.tsx +++ b/space/layouts/project-layout.tsx @@ -1,10 +1,9 @@ -import Image from "next/image"; - -// mobx import { observer } from "mobx-react-lite"; -import planeLogo from "public/plane-logo.svg"; +import Image from "next/image"; // components import IssueNavbar from "@/components/issues/navbar"; +// logo +import planeLogo from "public/plane-logo.svg"; const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
@@ -12,7 +11,6 @@ const ProjectLayout = ({ children }: { children: React.ReactNode }) => (
{children}
- = observer((props) => {
); + if (pageType === EPageTypes.PUBLIC) return <>{children}; if (pageType === EPageTypes.INIT) { diff --git a/space/lib/wrappers/instance-wrapper.tsx b/space/lib/wrappers/instance-wrapper.tsx index 05390fad8..3be92ed05 100644 --- a/space/lib/wrappers/instance-wrapper.tsx +++ b/space/lib/wrappers/instance-wrapper.tsx @@ -4,7 +4,7 @@ import useSWR from "swr"; // ui import { Spinner } from "@plane/ui"; // components -import { InstanceNotReady } from "@/components/instance"; +import { InstanceNotReady, InstanceFailureView } from "@/components/instance"; // hooks import { useInstance } from "@/hooks/store"; @@ -17,8 +17,11 @@ export const InstanceWrapper: FC = observer((props) => { // hooks const { isLoading, instance, fetchInstanceInfo } = useInstance(); - const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), { + const { isLoading: isSWRLoading, mutate } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), { revalidateOnFocus: false, + revalidateIfStale: false, + revalidateOnReconnect: false, + errorRetryCount: 0, }); if (isSWRLoading || isLoading) @@ -28,6 +31,8 @@ export const InstanceWrapper: FC = observer((props) => {
); + if (!instance) return ; + if (instance?.instance?.is_setup_done === false) return ; return <>{children}; diff --git a/space/public/instance/instance-failure-dark.svg b/space/public/instance/instance-failure-dark.svg new file mode 100644 index 000000000..58d691705 --- /dev/null +++ b/space/public/instance/instance-failure-dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/space/public/instance/instance-failure.svg b/space/public/instance/instance-failure.svg new file mode 100644 index 000000000..a59862283 --- /dev/null +++ b/space/public/instance/instance-failure.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/space/public/instance/plane-takeoff.png b/space/public/instance/plane-takeoff.png new file mode 100644 index 000000000..417ff8299 Binary files /dev/null and b/space/public/instance/plane-takeoff.png differ diff --git a/web/components/cycles/board/cycles-board-card.tsx b/web/components/cycles/board/cycles-board-card.tsx index ec6d80921..641007798 100644 --- a/web/components/cycles/board/cycles-board-card.tsx +++ b/web/components/cycles/board/cycles-board-card.tsx @@ -2,7 +2,7 @@ import { FC, MouseEvent, useRef } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { Info } from "lucide-react"; +import { CalendarCheck2, CalendarClock, Info, MoveRight } from "lucide-react"; // types import type { TCycleGroups } from "@plane/types"; // ui @@ -226,12 +226,14 @@ export const CyclesBoardCard: FC = observer((props) => {
- {isDateValid ? ( - - {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"} - - ) : ( - No due date + {isDateValid && ( +
+ + {renderFormattedDate(startDate)} + + + {renderFormattedDate(endDate)} +
)}
diff --git a/web/components/cycles/list/cycle-list-item-action.tsx b/web/components/cycles/list/cycle-list-item-action.tsx index 1f3d2ef65..05db2e2fa 100644 --- a/web/components/cycles/list/cycle-list-item-action.tsx +++ b/web/components/cycles/list/cycle-list-item-action.tsx @@ -1,6 +1,6 @@ import React, { FC, MouseEvent } from "react"; import { observer } from "mobx-react"; -import { User2 } from "lucide-react"; +import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react"; // types import { ICycle, TCycleGroups } from "@plane/types"; // ui @@ -106,9 +106,15 @@ export const CycleListItemAction: FC = observer((props) => { return ( <> -
- {renderDate && `${renderFormattedDate(startDate) ?? `_ _`} - ${renderFormattedDate(endDate) ?? `_ _`}`} -
+ {renderDate && ( +
+ + {renderFormattedDate(startDate)} + + + {renderFormattedDate(endDate)} +
+ )} {currentCycle && (
= observer((props) => { ) : progress === 100 ? ( ) : ( - {`${progress}%`} + {`${progress}%`} )} } diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index e9e89256b..3b6d40534 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; // icons -import { ArrowRight, Plus, PanelRight } from "lucide-react"; +import { ArrowRight, PanelRight } from "lucide-react"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -281,7 +281,6 @@ export const CycleIssuesHeader: React.FC = observer(() => { toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); }} size="sm" - prependIcon={} > Add Issue diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index e5c88d3f5..c2be61d82 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -1,8 +1,6 @@ import { FC } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -// icons -import { Plus } from "lucide-react"; // ui import { Breadcrumbs, Button, ContrastIcon } from "@plane/ui"; // components @@ -61,7 +59,6 @@ export const CyclesHeader: FC = observer(() => { )}
diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 5bbaa83e1..9a911103d 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -3,7 +3,7 @@ import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; // icons -import { ArrowRight, PanelRight, Plus } from "lucide-react"; +import { ArrowRight, PanelRight } from "lucide-react"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -288,7 +288,6 @@ export const ModuleIssuesHeader: React.FC = observer(() => { toggleCreateIssueModal(true, EIssuesStoreType.MODULE); }} size="sm" - prependIcon={} > Add Issue diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 881af67aa..90866d73e 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,7 +1,5 @@ import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; -// icons -import { Plus } from "lucide-react"; // ui import { Breadcrumbs, Button, DiceIcon } from "@plane/ui"; // components @@ -63,7 +61,6 @@ export const ModulesListHeader: React.FC = observer(() => {
{showButton && (
-
)} diff --git a/web/components/headers/pages.tsx b/web/components/headers/pages.tsx index aed050848..7ab9cb75d 100644 --- a/web/components/headers/pages.tsx +++ b/web/components/headers/pages.tsx @@ -1,6 +1,6 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { FileText, Plus } from "lucide-react"; +import { FileText } from "lucide-react"; // hooks // ui import { Breadcrumbs, Button } from "@plane/ui"; @@ -59,14 +59,13 @@ export const PagesHeader = observer(() => {
)} diff --git a/web/components/headers/project-inbox.tsx b/web/components/headers/project-inbox.tsx index 80a39862b..d61e2492d 100644 --- a/web/components/headers/project-inbox.tsx +++ b/web/components/headers/project-inbox.tsx @@ -1,7 +1,7 @@ import { FC, useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { Plus, RefreshCcw } from "lucide-react"; +import { RefreshCcw } from "lucide-react"; // ui import { Breadcrumbs, Button, LayersIcon } from "@plane/ui"; // components @@ -70,7 +70,7 @@ export const ProjectInboxHeader: FC = observer(() => { issue={undefined} /> - diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 9f64c0d3a..95983d85a 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; // icons -import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; +import { Briefcase, Circle, ExternalLink } from "lucide-react"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -229,7 +229,6 @@ export const ProjectIssuesHeader: React.FC = observer(() => { toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); }} size="sm" - prependIcon={} >
Add
Issue diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx index 1e21d10c7..297c976ee 100644 --- a/web/components/headers/project-view-issues.tsx +++ b/web/components/headers/project-view-issues.tsx @@ -2,8 +2,6 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; import { useRouter } from "next/router"; -// icons -import { Plus } from "lucide-react"; // types import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "@plane/types"; // ui @@ -241,7 +239,6 @@ export const ProjectViewIssuesHeader: React.FC = observer(() => { toggleCreateIssueModal(true, EIssuesStoreType.PROJECT_VIEW); }} size="sm" - prependIcon={} > Add Issue diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx index c79934aec..3cd578847 100644 --- a/web/components/headers/project-views.tsx +++ b/web/components/headers/project-views.tsx @@ -1,6 +1,5 @@ import { observer } from "mobx-react"; import { useRouter } from "next/router"; -import { Plus } from "lucide-react"; // hooks // components import { Breadcrumbs, PhotoFilterIcon, Button } from "@plane/ui"; @@ -61,13 +60,8 @@ export const ProjectViewsHeader: React.FC = observer(() => {
-
diff --git a/web/components/headers/projects.tsx b/web/components/headers/projects.tsx index ba75832e0..7126b2697 100644 --- a/web/components/headers/projects.tsx +++ b/web/components/headers/projects.tsx @@ -1,6 +1,6 @@ import { useCallback, useRef, useState } from "react"; import { observer } from "mobx-react"; -import { Search, Plus, Briefcase, X, ListFilter } from "lucide-react"; +import { Search, Briefcase, X, ListFilter } from "lucide-react"; // types import { TProjectFilters } from "@plane/types"; // ui @@ -167,7 +167,6 @@ export const ProjectsHeader = observer(() => { {isAuthorizedUser && ( - )} +
+
+ +
+ {issue.sub_issues_count > 0 && ( + + )} +
{displayProperties && displayProperties?.key && (
@@ -118,7 +121,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock
{issue?.is_draft ? ( - +

{issue.name}

) : ( @@ -132,7 +135,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock className="w-full truncate cursor-pointer text-sm text-custom-text-100" disabled={!!issue?.tempId} > - +

{issue.name}

@@ -151,7 +154,7 @@ export const IssueBlock: React.FC = observer((props: IssueBlock {!issue?.tempId ? ( <> = (props) => { (_list: IGroupByColumn) => validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
-
+
= (props) => { onClose={() => setViewModal(false)} /> -
diff --git a/web/components/issues/sub-issues/issues-list.tsx b/web/components/issues/sub-issues/issues-list.tsx index 1c08a2b9e..2ca847624 100644 --- a/web/components/issues/sub-issues/issues-list.tsx +++ b/web/components/issues/sub-issues/issues-list.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, Fragment } from "react"; import { observer } from "mobx-react-lite"; import { TIssue } from "@plane/types"; // hooks @@ -45,7 +45,7 @@ export const IssueList: FC = observer((props) => { {subIssueIds && subIssueIds.length > 0 && subIssueIds.map((issueId) => ( - <> + = observer((props) => { handleIssueCrudState={handleIssueCrudState} subIssueOperations={subIssueOperations} /> - + ))}
= observer((props) => { return ( <> {/* page details */} -
- {/* Labels - */} -
- - - -
- - {/* 10m read - */} -
- - {access === 0 ? : } - -
+
+ + + +
+
+ + {access === 0 ? : } +
- {/* vertical divider */} diff --git a/web/components/ui/loader/layouts/list-layout-loader.tsx b/web/components/ui/loader/layouts/list-layout-loader.tsx index 9b861cc97..7285886a3 100644 --- a/web/components/ui/loader/layouts/list-layout-loader.tsx +++ b/web/components/ui/loader/layouts/list-layout-loader.tsx @@ -1,3 +1,4 @@ +import { Fragment } from "react"; import { getRandomInt, getRandomLength } from "../utils"; const ListItemRow = () => ( @@ -8,13 +9,13 @@ const ListItemRow = () => (
{[...Array(6)].map((_, index) => ( - <> + {getRandomInt(1, 2) % 2 === 0 ? ( ) : ( )} - + ))}
diff --git a/web/store/member/index.ts b/web/store/member/index.ts index 65b35f76a..958fb7ead 100644 --- a/web/store/member/index.ts +++ b/web/store/member/index.ts @@ -1,7 +1,7 @@ import { action, makeObservable, observable } from "mobx"; // types -import { RootStore } from "@/store/root.store"; import { IUserLite } from "@plane/types"; +import { RootStore } from "@/store/root.store"; import { IProjectMemberStore, ProjectMemberStore } from "./project-member.store"; import { IWorkspaceMemberStore, WorkspaceMemberStore } from "./workspace-member.store"; diff --git a/yarn.lock b/yarn.lock index c25eddcda..053bb68d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6908,15 +6908,6 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@8.4.23: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@8.4.31: version "8.4.31" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" @@ -6926,7 +6917,7 @@ postcss@8.4.31: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.29: +postcss@^8.4.23, postcss@^8.4.38: version "8.4.38" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -7954,16 +7945,8 @@ streamx@^2.15.0, streamx@^2.16.1: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8043,14 +8026,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==