diff --git a/playwright-tests/tests/addons.spec.js b/playwright-tests/tests/addons.spec.js
new file mode 100644
index 000000000..f740f1dc5
--- /dev/null
+++ b/playwright-tests/tests/addons.spec.js
@@ -0,0 +1,124 @@
+const { test, expect } = require("@playwright/test");
+
+test.describe("Wallet is connected", () => {
+ test.use({
+ storageState: "playwright-tests/storage-states/wallet-connected.json",
+ });
+
+ test.describe("AddonsConfigurator", () => {
+ const baseUrl =
+ "/devgovgigs.near/widget/app?page=community.configuration&handle=devhub-test";
+ // const dropdownSelector =
+ // 'input[data-component="near/widget/DIG.InputSelect"]';
+ // const addButtonSelector = "button.btn-success:has(i.bi.bi-plus)";
+ // const toggleButtonSelector = 'button[role="switch"]';
+ // const moveUpButtonSelector = "button.btn-secondary:has(i.bi.bi-arrow-up)";
+ // const removeButtonSelector =
+ // "button.btn-outline-danger:has(i.bi.bi-trash-fill)";
+
+ test("Addons configuration section comes up on load", async ({ page }) => {
+ await page.goto(baseUrl);
+
+ const addonsConfiguratorSelector = 'span:has-text("Add-Ons")';
+
+ await page.waitForSelector(addonsConfiguratorSelector, {
+ state: "visible",
+ });
+ });
+ });
+});
+
+// NOTE:
+// The below tests need some more work.
+// We are using the DIG component, which is essentially a Radix Select field.
+// Radix select renders as an input and requires to be focused, then value inputted, before the option can be selected.
+// I was having trouble making this work.
+
+// test('Can add an addon to the list', async ({ page }) => {
+// await page.goto(baseUrl);
+
+// await page.click(dropdownSelector);
+// await page.fill(dropdownSelector, 'Wiki');
+// await page.click(`li:has-text('Wiki')`);
+
+// await page.waitForSelector(addButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(addButtonSelector);
+
+// const addedAddon = await page.$('input[type="text"].form-control[disabled][value="Wiki"]'); // You will need to fill this in
+// expect(addedAddon).toBeTruthy();
+// });
+
+// test('Can reorder addons in the list', async ({ page }) => {
+// await page.goto(baseUrl);
+
+// await page.click(dropdownSelector);
+// await page.inputValue(dropdownSelector, 'Wiki');
+// await page.click(`li:has-text('Wiki')`);
+
+// await page.waitForSelector(addButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(addButtonSelector);
+
+// await page.inputValue(dropdownSelector, 'telegram');
+// await page.click(addButtonSelector);
+
+// await page.waitForSelector(moveUpButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(moveUpButtonSelector);
+
+// const firstAddon = await page.$('input[type="text"].form-control[disabled][value="Telegram"]');
+// const secondAddon = await page.$('input[type="text"].form-control[disabled][value="Wiki"]');
+// expect(firstAddon).toBeTruthy();
+// expect(secondAddon).toBeTruthy();
+// });
+
+// test('Can remove an addon from the list', async ({ page }) => {
+// await page.goto(baseUrl);
+
+// await page.inputValue(dropdownSelector, 'Wiki');
+
+// await page.waitForSelector(addButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(addButtonSelector);
+
+// await page.waitForSelector(removeButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(removeButtonSelector);
+
+// const removedAddon = await page.$('input[type="text"].form-control[disabled][value="Wiki"]'); // You will need to fill this in
+// expect(removedAddon).toBeNull();
+// });
+
+// test('Can toggle to disable an addon', async ({ page }) => {
+// await page.goto(baseUrl);
+
+// await page.inputValue(dropdownSelector, 'Wiki');
+// await page.waitForSelector(addButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(addButtonSelector);
+
+// await page.waitForSelector(toggleButtonSelector, {
+// state: "visible",
+// });
+
+// await page.click(toggleButtonSelector);
+
+// const toggleState = await page.getAttribute(toggleButtonSelector, 'aria-checked');
+// expect(toggleState).toBe('false');
+// });
+// });
+
+// });
diff --git a/playwright-tests/tests/search.spec.js b/playwright-tests/tests/search.spec.js
index 8e6cc5bf2..be967b90c 100644
--- a/playwright-tests/tests/search.spec.js
+++ b/playwright-tests/tests/search.spec.js
@@ -9,6 +9,7 @@ test("should show post history for posts in the feed", async ({ page }) => {
state: "visible",
});
await searchInput.fill("zero knowledge");
+ await searchInput.press("Enter");
await page.waitForSelector('span:has-text("zero knowledge")', {
state: "visible",
diff --git a/src/core/adapter/devhub-contract.jsx b/src/core/adapter/devhub-contract.jsx
index eb74d0f13..fbbac46d9 100644
--- a/src/core/adapter/devhub-contract.jsx
+++ b/src/core/adapter/devhub-contract.jsx
@@ -113,30 +113,30 @@ function getAvailableAddons() {
configurator_widget:
"${REPL_DEVHUB}/widget/devhub.entity.addon.telegram.Configurator",
},
- {
- id: "github",
- title: "Github",
- description: "Connect your github",
- view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.github.Viewer",
- configurator_widget:
- "${REPL_DEVHUB}/widget/devhub.entity.addon.github.Configurator",
- },
- {
- id: "kanban",
- title: "Kanban",
- description: "Connect your github kanban board",
- view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.kanban.Viewer",
- configurator_widget:
- "${REPL_DEVHUB}/widget/devhub.entity.addon.kanban.Configurator",
- },
- {
- id: "blog",
- title: "Blog",
- description: "Create a blog for your community",
- view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.blog.Viewer",
- configurator_widget:
- "${REPL_DEVHUB}/widget/devhub.entity.addon.blog.Configurator",
- },
+ // {
+ // id: "github",
+ // title: "Github",
+ // description: "Connect your github",
+ // view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.github.Viewer",
+ // configurator_widget:
+ // "${REPL_DEVHUB}/widget/devhub.entity.addon.github.Configurator",
+ // },
+ // {
+ // id: "kanban",
+ // title: "Kanban",
+ // description: "Connect your github kanban board",
+ // view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.kanban.Viewer",
+ // configurator_widget:
+ // "${REPL_DEVHUB}/widget/devhub.entity.addon.kanban.Configurator",
+ // },
+ // {
+ // id: "blog",
+ // title: "Blog",
+ // description: "Create a blog for your community",
+ // view_widget: "${REPL_DEVHUB}/widget/devhub.entity.addon.blog.Viewer",
+ // configurator_widget:
+ // "${REPL_DEVHUB}/widget/devhub.entity.addon.blog.Configurator",
+ // },
];
// return Near.view("${REPL_DEVHUB_CONTRACT}", "get_available_addons") ?? null;
}
diff --git a/src/devhub/components/molecule/Input.jsx b/src/devhub/components/molecule/Input.jsx
new file mode 100644
index 000000000..69b27c7e2
--- /dev/null
+++ b/src/devhub/components/molecule/Input.jsx
@@ -0,0 +1,99 @@
+const TextInput = ({
+ className,
+ format,
+ inputProps: { className: inputClassName, ...inputProps },
+ key,
+ label,
+ multiline,
+ onChange,
+ placeholder,
+ type,
+ value,
+ skipPaddingGap,
+ style,
+ ...otherProps
+}) => {
+ const typeAttribute =
+ type === "text" || type === "password" || type === "number" ? type : "text";
+
+ const renderedLabels = [
+ (label?.length ?? 0) > 0 ? (
+
+ {label}
+
+ {inputProps.required ? * : null}
+
+ ) : null,
+
+ format === "markdown" ? (
+
+ ) : null,
+
+ format === "comma-separated" ? (
+
+ {format}
+
+ ) : null,
+
+ (inputProps.max ?? null) !== null ? (
+ {`${
+ value?.length ?? 0
+ } / ${inputProps.max}`}
+ ) : null,
+ ].filter((label) => label !== null);
+
+ return (
+
+ {renderedLabels.length > 0 ? (
+
+ {renderedLabels.map((label) => label)}
+
+ ) : null}
+
+ {!multiline ? (
+
+ {inputProps.prefix && (
+ {inputProps.prefix}
+ )}
+
+
+ ) : (
+
+ )}
+
+ );
+};
+
+return TextInput(props);
diff --git a/src/devhub/components/templates/AppLayout.jsx b/src/devhub/components/templates/AppLayout.jsx
index fbb4175b3..741771f15 100644
--- a/src/devhub/components/templates/AppLayout.jsx
+++ b/src/devhub/components/templates/AppLayout.jsx
@@ -109,7 +109,6 @@ const AppHeader = ({ page }) => {
};
function AppLayout({ page, children }) {
- console.log("page", page);
return (
diff --git a/src/devhub/entity/addon/telegram/Configurator.jsx b/src/devhub/entity/addon/telegram/Configurator.jsx
new file mode 100644
index 000000000..3ac05e79a
--- /dev/null
+++ b/src/devhub/entity/addon/telegram/Configurator.jsx
@@ -0,0 +1,117 @@
+const { Tile } =
+ VM.require("${REPL_DEVHUB}/widget/devhub.components.molecule.Tile") ||
+ (() => <>>);
+
+const { data, onSubmit } = props;
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+const Item = styled.div`
+ padding: 10px;
+ margin: 5px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: 10px;
+`;
+
+const EditableField = styled.input`
+ flex: 1;
+`;
+const initialData = data.handles;
+const [handles, setHandles] = useState(initialData || []);
+const [newItem, setNewItem] = useState("");
+
+const handleAddItem = () => {
+ if (newItem) {
+ setHandles([...handles, newItem]);
+ setNewItem("");
+ }
+};
+
+const handleDeleteItem = (index) => {
+ const updatedData = [...handles];
+ updatedData.splice(index, 1);
+ setHandles(updatedData);
+};
+
+const handleSubmit = () => {
+ onSubmit({ handles: handles.map((handle) => handle.trim()) });
+};
+
+return (
+
+
+ {handles.map((item, index) => (
+ -
+
+
+
+
+
+ ))}
+ -
+
+ setNewItem(e.target.value),
+ value: newItem,
+ placeholder: "Telegram Handle",
+ inputProps: {
+ prefix: "https://t.me/",
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+);
diff --git a/src/devhub/entity/addon/telegram/Viewer.jsx b/src/devhub/entity/addon/telegram/Viewer.jsx
new file mode 100644
index 000000000..4959daf74
--- /dev/null
+++ b/src/devhub/entity/addon/telegram/Viewer.jsx
@@ -0,0 +1,51 @@
+const { handles } = props;
+
+const CenteredMessage = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: ${(p) => p.height ?? "100%"};
+`;
+
+if (!handles || handles.length === 0) {
+ return (
+
+ No Telegram Configured
+
+ );
+} else {
+ return (
+
+ {(handles || []).map((tg) => (
+ <>
+
+
+
+
+
+ >
+ ))}
+
+ );
+}
diff --git a/src/devhub/entity/addon/wiki/Configurator.jsx b/src/devhub/entity/addon/wiki/Configurator.jsx
new file mode 100644
index 000000000..6ce7ef1a6
--- /dev/null
+++ b/src/devhub/entity/addon/wiki/Configurator.jsx
@@ -0,0 +1,163 @@
+const { data, onSubmit } = props;
+
+const initialData = data;
+const [content, setContent] = useState(data.content || "");
+const [title, setTitle] = useState(data.title || "");
+const [description, setDescription] = useState(data.description || "");
+const [textAlign, setTextAlign] = useState(data.textAlign || "left");
+
+const Container = styled.div`
+ width: 100%;
+ margin: 0 auto;
+ padding: 20px;
+ text-align: left;
+`;
+
+const FormContainer = styled.div`
+ padding-top: 1rem;
+
+ & > *:not(:last-child) {
+ margin-bottom: 1rem;
+ }
+`;
+
+const hasDataChanged = () => {
+ return (
+ content !== initialData.content ||
+ title !== initialData.title ||
+ description !== initialData.description ||
+ textAlign !== initialData.textAlign
+ );
+};
+
+const handleSubmit = () => {
+ if (title) onSubmit({ title, description, content, textAlign });
+};
+
+return (
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+ setTextAlign(e.target.value),
+ options: [
+ { label: "Left", value: "left" },
+ { label: "Center", value: "center" },
+ { label: "Right", value: "right" },
+ ],
+ }}
+ />
+
+
+
+ setTitle(e.target.value),
+ value: title,
+ inputProps: {
+ min: 2,
+ max: 60,
+ required: true,
+ },
+ }}
+ />
+
+
+ setDescription(e.target.value),
+ value: description,
+ inputProps: {
+ min: 2,
+ max: 250,
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/src/devhub/entity/addon/wiki/Viewer.jsx b/src/devhub/entity/addon/wiki/Viewer.jsx
new file mode 100644
index 000000000..75b0ce977
--- /dev/null
+++ b/src/devhub/entity/addon/wiki/Viewer.jsx
@@ -0,0 +1,52 @@
+const { content, title, description, textAlign } = props;
+
+const Container = styled.div`
+ width: 100%;
+ margin: 0 auto;
+ text-align: ${(p) => p.textAlign ?? "left"};
+`;
+
+const Content = styled.div`
+ margin: 20px 0;
+ text-align: left;
+`;
+
+const Title = styled.h1`
+ margin-bottom: 10px; /* Optional: Adjust margin as needed */
+`;
+
+const Description = styled.p`
+ margin-bottom: 20px; /* Optional: Adjust margin as needed */
+`;
+
+const CenteredMessage = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: ${(p) => p.height ?? "100%"};
+`;
+
+if (!content) {
+ return (
+
+ No Wiki Configured
+
+ );
+} else {
+ return (
+
+ {title}
+ {description}
+
+
+
+
+ );
+}
diff --git a/src/devhub/entity/community/Provider.jsx b/src/devhub/entity/community/Provider.jsx
index bd02872e5..03689c164 100644
--- a/src/devhub/entity/community/Provider.jsx
+++ b/src/devhub/entity/community/Provider.jsx
@@ -6,6 +6,7 @@ const {
updateCommunity,
deleteCommunity,
getCommunity,
+ setCommunityAddons,
} = VM.require("${REPL_DEVHUB}/widget/core.adapter.devhub-contract");
if (
@@ -13,7 +14,8 @@ if (
!getAccountCommunityPermissions ||
!createCommunity ||
!updateCommunity ||
- !deleteCommunity
+ !deleteCommunity ||
+ !setCommunityAddons
) {
return Loading modules...
;
}
@@ -62,6 +64,7 @@ return (
{});
+
+const { href } = VM.require("${REPL_DEVHUB}/widget/core.lib.url") || (() => {});
+
+const availableAddons = getAvailableAddons() || [];
+
+const isActive = props.isActive;
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+const Item = styled.div`
+ padding: 10px;
+ margin: 5px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+`;
+
+const Icon = styled.span`
+ margin-right: 10px;
+`;
+
+const EditableField = styled.input`
+ flex: 1;
+`;
+
+const ToggleButton = styled.input`
+ margin-left: 10px;
+`;
+
+const Table = styled.table`
+ width: 100%;
+ border-collapse: collapse;
+`;
+
+const Header = styled.thead`
+ background-color: #f0f0f0;
+`;
+
+const HeaderCell = styled.th`
+ padding: 10px;
+ text-align: left;
+`;
+
+const Row = styled.tr``;
+
+const Cell = styled.td`
+ padding: 10px;
+`;
+
+function generateRandom6CharUUID() {
+ const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
+ let result = "";
+
+ for (let i = 0; i < 6; i++) {
+ const randomIndex = Math.floor(Math.random() * chars.length);
+ result += chars[randomIndex];
+ }
+
+ return result;
+}
+
+const AddonItem = ({
+ data,
+ onUpdate,
+ onMove,
+ onRemove,
+ index,
+ isTop,
+ isBottom,
+}) => {
+ const handleNameChange = (event) => {
+ const newName = event.target.value;
+ onUpdate({ ...data, display_name: newName });
+ };
+
+ const handleEnableChange = () => {
+ onUpdate({ ...data, enabled: !data.enabled });
+ };
+
+ const moveItemUp = () => {
+ if (!isTop) {
+ onMove(index, index - 1);
+ }
+ };
+
+ const moveItemDown = () => {
+ if (!isBottom) {
+ onMove(index, index + 1);
+ }
+ };
+
+ const removeItem = () => {
+ onRemove(data.id);
+ };
+
+ const addonMatch =
+ availableAddons.find((it) => it.id === data.addon_id) ?? null;
+
+ return (
+
+
+
+
+
+
+ |
+
+ {addonMatch.title}
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+ {isActive && (
+
+ )}
+
+ |
+
+ );
+};
+
+function arraysAreEqual(arr1, arr2) {
+ if (arr1.length !== arr2.length) {
+ return false;
+ }
+ for (let i = 0; i < arr1.length; i++) {
+ if (arr1[i] !== arr2[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+const AddonsConfigurator = ({ data, onSubmit }) => {
+ const [originalList, setOriginalList] = useState(data);
+ const [list, setList] = useState(data);
+ const [changesMade, setChangesMade] = useState(false);
+
+ useEffect(() => {
+ setOriginalList(data);
+ }, [data]);
+
+ const updateItem = (updatedItem) => {
+ const updatedList = list.map((item) =>
+ item.id === updatedItem.id ? updatedItem : item
+ );
+ setList(updatedList);
+ setChangesMade(!arraysAreEqual(originalList, updatedList));
+ };
+
+ const moveItem = (fromIndex, toIndex) => {
+ const updatedList = [...list];
+ const [movedItem] = updatedList.splice(fromIndex, 1);
+ updatedList.splice(toIndex, 0, movedItem);
+ setList(updatedList);
+ setChangesMade(!arraysAreEqual(originalList, updatedList));
+ };
+
+ const [selectedAddon, setSelectedAddon] = useState(null);
+
+ const handleAddItem = () => {
+ const newItem = {
+ id: generateRandom6CharUUID(),
+ addon_id: selectedAddon.id,
+ display_name: selectedAddon.title,
+ enabled: true,
+ parameters: "{}",
+ };
+ const updatedList = [...list, newItem];
+ setList(updatedList);
+ setChangesMade(!arraysAreEqual(originalList, updatedList));
+ };
+
+ const removeItem = (id) => {
+ const updatedList = list.filter((item) => item.id !== id);
+ setList(updatedList);
+ setChangesMade(!arraysAreEqual(originalList, updatedList));
+ };
+
+ return (
+
+
+ Add or remove custom tabs, which will appear in your community's
+ navigation bar.
+
+ You can customize them on each page.
+
+
+
+
+ Order
+ Tab Type
+ Tab Name
+ Enabled
+ Actions
+
+
+
+ {list.map((item, index) => (
+
+ ))}
+
+
+ {isActive && availableAddons && list.length < 7 && (
+
+
+ ({
+ label: it.title,
+ value: it.id,
+ })),
+ },
+ ],
+ rootProps: {
+ value: selectedAddon.id ?? null,
+ placeholder: "Select an addon",
+ onValueChange: (value) =>
+ setSelectedAddon(
+ (availableAddons || []).find((it) => it.id === value)
+ ),
+ },
+ }}
+ />
+
+
+
+ )}
+ {isActive && (
+
+ onSubmit(list),
+ }}
+ />
+
+ )}
+
+ );
+};
+
+return AddonsConfigurator(props);
diff --git a/src/devhub/page/addon.jsx b/src/devhub/page/addon.jsx
new file mode 100644
index 000000000..7f9d87b93
--- /dev/null
+++ b/src/devhub/page/addon.jsx
@@ -0,0 +1,147 @@
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ position: relative;
+`;
+
+const Content = styled.div`
+ flex: 1;
+ padding: 20px;
+ overflow: auto;
+`;
+
+const SettingsButton = styled.button`
+ position: absolute;
+ top: 10px;
+ right: 10px;
+
+ background-color: #fff;
+ display: flex;
+ padding: 14px 16px;
+ align-items: center;
+ gap: 16px;
+ width: 50px;
+ height: 50px;
+
+ border-radius: 4px;
+ border: 1px solid #00ec97;
+
+ z-index: 10;
+
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+
+ &:hover {
+ transform: translateY(2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ &:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+ }
+`;
+
+const CenteredMessage = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: ${(p) => p.height ?? "100%"};
+`;
+
+const { addon, permissions, handle } = props;
+
+const { getAvailableAddons, setCommunityAddon } = VM.require(
+ "${REPL_DEVHUB}/widget/core.adapter.devhub-contract"
+);
+
+if (!getAvailableAddons || !setCommunityAddon) {
+ return Loading modules...
;
+}
+
+const availableAddons = getAvailableAddons();
+const addonMatch = availableAddons.find((it) => it.id === addon.addon_id);
+
+if (!addonMatch) {
+ return (
+
+ Addon with id: "{addon.addon_id}" not found.
+
+ );
+}
+
+const config = JSON.parse(addon.parameters || "null");
+
+const ButtonRow = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
+const [view, setView] = useState(props.view || "viewer");
+
+const checkFullyRefactored = (addon_id) => {
+ switch (addon_id) {
+ case "kanban":
+ case "github":
+ return false;
+ default:
+ return true;
+ }
+};
+
+const isFullyRefactored = checkFullyRefactored(addon.addon_id);
+
+return (
+
+ {isFullyRefactored && // Unfully refactored addons have the configurator built in.
+ // So we hide the header
+ permissions.can_configure && (
+ setView(view === "configure" ? "view" : "configure")}
+ >
+ {view === "configure" ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* We hide in order to prevent a reload when we switch between two views */}
+
+ {
+ setCommunityAddon({
+ handle,
+ addon: {
+ ...addon,
+ parameters: JSON.stringify(data),
+ },
+ });
+ },
+
+ handle, // this is temporary prop drilling until kanban and github are migrated
+ permissions,
+ }}
+ />
+
+
+
+
+
+
+);
diff --git a/src/devhub/page/community/configuration.jsx b/src/devhub/page/community/configuration.jsx
index b5887726d..40393461c 100644
--- a/src/devhub/page/community/configuration.jsx
+++ b/src/devhub/page/community/configuration.jsx
@@ -2,10 +2,17 @@ const { Tile } =
VM.require("${REPL_DEVHUB}/widget/devhub.components.molecule.Tile") ||
(() => <>>);
-const { permissions, handle, community, deleteCommunity, updateCommunity } =
- props;
+const {
+ permissions,
+ handle,
+ community,
+ setCommunityAddons,
+ deleteCommunity,
+ updateCommunity,
+} = props;
-const [communityData, setCommunityData] = useState(community);
+const [communityData, setCommunityData] = useState(community || {});
+const [selectedAddon, setSelectedAddon] = useState(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const sectionSubmit = (sectionData) => {
@@ -33,7 +40,7 @@ const hasDeletePermissions = permissions.can_delete;
return (
+ {hasConfigurePermissions && (
+
+ (
+ setCommunityAddons({ handle, addons: v }),
+ ...p,
+ }}
+ />
+ ),
+ }}
+ />
+
+ )}
{hasDeletePermissions && (
{
+ addon.enabled &&
+ tabs.push({
+ title: addon.display_name,
+ view: "${REPL_DEVHUB}/widget/devhub.page.addon",
+ params: { addon },
+ });
+});
+
const onShareClick = () =>
clipboard
.writeText(
@@ -174,7 +183,17 @@ return (
{currentTab && (
-
+
)}