Display featured applications in marketplace (#22635)

## After
<img width="1060" height="589" alt="image"
src="https://github.com/user-attachments/assets/74dfadcf-8698-4404-81c6-b309cc4cbf79"
/>
<img width="732" alt="image"
src="https://github.com/user-attachments/assets/0e1a3644-04bc-4208-aa77-3842d9db9cc8"
/>
<img width="797" alt="image"
src="https://github.com/user-attachments/assets/0456ecce-607a-4705-8a89-c77029bfb6ac"
/>

- Remove IS_MARKETPLACE_SETTING_TAB_VISIBLE feature flag
- add vetted toggle in admin app tab
- added people data labs, last contact and call recorder to default
vetted applications

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22635?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
martmull
2026-07-07 17:05:54 +02:00
committed by GitHub
parent 2c0e0b2eac
commit 435073e9c5
24 changed files with 161 additions and 177 deletions
@@ -1,6 +1,6 @@
{
"name": "@twentyhq/call-recorder",
"version": "1.0.7",
"version": "1.0.8",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -43,6 +43,7 @@ export default defineApplication({
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
logoUrl: 'public/logo.svg',
category: 'Productivity',
author: 'Twenty',
screenshots: ['public/gallery/call-recorder-cover.png'],
applicationVariables: {
@@ -1,6 +1,6 @@
{
"name": "@twentyhq/people-data-labs",
"version": "1.0.4",
"version": "1.0.5",
"description": "People Data Labs enrichment app for Twenty",
"license": "MIT",
"engines": {
@@ -7,6 +7,7 @@ export default defineApplication({
displayName: 'People Data Labs',
description: 'Enrich People and Companies with People Data Labs data.',
logoUrl: 'public/people-data-labs-icon.png',
category: 'Enrichment',
author: 'Twenty',
screenshots: ['public/gallery/cover.png'],
serverVariables: {
@@ -1,6 +1,6 @@
{
"name": "@twentyhq/last-contact",
"version": "1.0.2",
"version": "1.0.3",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -10,6 +10,7 @@ export default defineApplication({
universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER,
logoUrl: 'public/logo.png',
author: 'Twenty',
category: 'Productivity',
screenshots: ['public/gallery/cover.png'],
displayName: APP_DISPLAY_NAME,
description: APP_DESCRIPTION,
@@ -1752,7 +1752,6 @@ type FeatureFlag {
enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED
IS_JSON_FILTER_ENABLED
IS_MARKETPLACE_SETTING_TAB_VISIBLE
IS_EMAIL_GROUP_ENABLED
IS_JUNCTION_RELATIONS_ENABLED
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
@@ -4326,6 +4325,7 @@ input UpdateApplicationRegistrationPayload {
oAuthScopes: [String!]
isListed: Boolean
isPreInstalled: Boolean
isFeatured: Boolean
}
input CreateApplicationRegistrationVariableInput {
@@ -1396,7 +1396,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -6575,7 +6575,7 @@ export interface CreateApplicationRegistrationInput {name: Scalars['String'],uni
export interface UpdateApplicationRegistrationInput {id: Scalars['String'],update: UpdateApplicationRegistrationPayload}
export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),isListed?: (Scalars['Boolean'] | null),isPreInstalled?: (Scalars['Boolean'] | null)}
export interface UpdateApplicationRegistrationPayload {name?: (Scalars['String'] | null),oAuthRedirectUris?: (Scalars['String'][] | null),oAuthScopes?: (Scalars['String'][] | null),isListed?: (Scalars['Boolean'] | null),isPreInstalled?: (Scalars['Boolean'] | null),isFeatured?: (Scalars['Boolean'] | null)}
export interface CreateApplicationRegistrationVariableInput {applicationRegistrationId: Scalars['String'],key: Scalars['String'],value: Scalars['String'],description?: (Scalars['String'] | null),isSecret?: (Scalars['Boolean'] | null)}
@@ -9238,7 +9238,6 @@ export const enumLogicFunctionExecutionStatus = {
export const enumFeatureFlagKey = {
IS_UNIQUE_INDEXES_ENABLED: 'IS_UNIQUE_INDEXES_ENABLED' as const,
IS_JSON_FILTER_ENABLED: 'IS_JSON_FILTER_ENABLED' as const,
IS_MARKETPLACE_SETTING_TAB_VISIBLE: 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' as const,
IS_EMAIL_GROUP_ENABLED: 'IS_EMAIL_GROUP_ENABLED' as const,
IS_JUNCTION_RELATIONS_ENABLED: 'IS_JUNCTION_RELATIONS_ENABLED' as const,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
@@ -11096,6 +11096,9 @@ export default {
"isPreInstalled": [
6
],
"isFeatured": [
6
],
"__typename": [
1
]
@@ -317,7 +317,6 @@ export enum FeatureFlagKey {
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED = 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
@@ -834,6 +833,7 @@ export type UpdateApplicationRegistrationInput = {
};
export type UpdateApplicationRegistrationPayload = {
isFeatured?: InputMaybe<Scalars['Boolean']['input']>;
isListed?: InputMaybe<Scalars['Boolean']['input']>;
isPreInstalled?: InputMaybe<Scalars['Boolean']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
@@ -1756,7 +1756,6 @@ export enum FeatureFlagKey {
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED = 'IS_MESSAGING_CALENDAR_WEBHOOK_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
@@ -5347,6 +5346,7 @@ export type UpdateApplicationRegistrationInput = {
};
export type UpdateApplicationRegistrationPayload = {
isFeatured?: InputMaybe<Scalars['Boolean']['input']>;
isListed?: InputMaybe<Scalars['Boolean']['input']>;
isPreInstalled?: InputMaybe<Scalars['Boolean']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
@@ -1,7 +1,12 @@
import { Section } from 'twenty-ui/layout';
import { AppTooltip, Card, TooltipDelay } from 'twenty-ui/surfaces';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { IconArrowBarToDown, IconPinned, IconReload } from 'twenty-ui/icon';
import {
IconArrowBarToDown,
IconPinned,
IconReload,
IconShield,
} from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { H2Title } from 'twenty-ui/typography';
import { type ApplicationRegistration } from '~/generated-metadata/graphql';
@@ -104,6 +109,24 @@ export const SettingsAdminApplicationRegistrationGeneralToggles = ({
}
/>
</Card>
<Card rounded fullWidth>
<SettingsOptionCardContentToggle
Icon={IconShield}
title={t`Vetted`}
description={t`Mark this app as reviewed and approved`}
checked={registration.isFeatured}
onChange={(checked) =>
updateRegistration({
variables: {
input: {
id: registration.id,
update: { isFeatured: checked },
},
},
})
}
/>
</Card>
<Card rounded fullWidth>
<SettingsOptionCardContentToggle
Icon={IconPinned}
@@ -4,7 +4,6 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { SettingsTabBar } from '@/settings/components/layout/SettingsTabBar';
import { useSettingsActiveTabId } from '@/settings/components/layout/useSettingsActiveTabId';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
@@ -12,10 +11,7 @@ import { IconApps, IconCode, IconDownload, IconPlug } from 'twenty-ui/icon';
import { Section } from 'twenty-ui/layout';
import coverDark from '~/pages/settings/applications/assets/cover-dark.png';
import coverLight from '~/pages/settings/applications/assets/cover-light.png';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { PermissionFlagType } from '~/generated-metadata/graphql';
import { SettingsApplicationsAvailableTab } from '~/pages/settings/applications/tabs/SettingsApplicationsAvailableTab';
import { SettingsApplicationsDeveloperTab } from '~/pages/settings/applications/tabs/SettingsApplicationsDeveloperTab';
import { SettingsApplicationsInstalledTab } from '~/pages/settings/applications/tabs/SettingsApplicationsInstalledTab';
@@ -30,14 +26,8 @@ export const SettingsApplications = () => {
PermissionFlagType.API_KEYS_AND_WEBHOOKS,
);
const isMarketplaceSettingTabVisible = useIsFeatureEnabled(
FeatureFlagKey.IS_MARKETPLACE_SETTING_TAB_VISIBLE,
);
const tabs = [
...(isMarketplaceSettingTabVisible
? [{ id: 'marketplace', title: t`Marketplace`, Icon: IconDownload }]
: []),
{ id: 'marketplace', title: t`Marketplace`, Icon: IconDownload },
{ id: 'installed', title: t`Installed`, Icon: IconApps },
...(hasDeveloperAccess
? [{ id: 'developer', title: t`Developer`, Icon: IconCode }]
@@ -58,11 +48,7 @@ export const SettingsApplications = () => {
case 'developer':
return <SettingsApplicationsDeveloperTab />;
default:
return isMarketplaceSettingTabVisible ? (
<SettingsApplicationsAvailableTab />
) : (
<SettingsApplicationsInstalledTab />
);
return <SettingsApplicationsAvailableTab />;
}
};
@@ -5,6 +5,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { type ReactNode, useState } from 'react';
import { InlineBanner } from 'twenty-ui/feedback';
import { IconSparkles } from 'twenty-ui/icon';
import { SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
@@ -17,6 +18,16 @@ const StyledSearchInputContainer = styled.div`
padding-bottom: ${themeCssVariables.spacing[2]};
`;
const StyledContentContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[3]};
`;
const StyledNotFeaturedContainer = styled.div`
margin-top: ${themeCssVariables.spacing[3]};
`;
const StyledCardsGrid = styled.div`
display: grid;
gap: ${themeCssVariables.spacing[3]};
@@ -56,14 +67,13 @@ export const SettingsApplicationsAvailableTab = () => {
})
: marketplaceApps;
const filteredApplications = showFeaturedOnly
? textFilteredApplications.filter((application) => application.isFeatured)
: textFilteredApplications;
const featuredApplications = textFilteredApplications.filter(
(application) => application.isFeatured,
);
const nonFeaturedCount = showFeaturedOnly
? textFilteredApplications.filter((application) => !application.isFeatured)
.length
: 0;
const nonFeaturedApplications = textFilteredApplications.filter(
(application) => !application.isFeatured,
);
if (isLoading) {
return (
@@ -74,7 +84,11 @@ export const SettingsApplicationsAvailableTab = () => {
}
const showNonFeaturedHint =
filteredApplications.length === 0 && nonFeaturedCount > 0;
showFeaturedOnly &&
featuredApplications.length === 0 &&
nonFeaturedApplications.length > 0;
const hasNoApplications = textFilteredApplications.length === 0;
return (
<Section>
@@ -109,10 +123,11 @@ export const SettingsApplicationsAvailableTab = () => {
/>
</StyledSearchInputContainer>
{filteredApplications.length === 0 ? (
{hasNoApplications ||
(showFeaturedOnly && featuredApplications.length === 0) ? (
<SettingsEmptyPlaceholder padding="4">
{showNonFeaturedHint
? t`No featured applications found. ${nonFeaturedCount} non-featured result(s) available — `
? t`No featured applications found. ${nonFeaturedApplications.length} non-featured result(s) available — `
: t`No applications available`}
{showNonFeaturedHint && (
<StyledHintLink onClick={() => setShowFeaturedOnly(false)}>
@@ -121,14 +136,35 @@ export const SettingsApplicationsAvailableTab = () => {
)}
</SettingsEmptyPlaceholder>
) : (
<StyledCardsGrid>
{filteredApplications.map((application) => (
<SettingsAvailableApplicationCard
key={application.id}
application={application}
/>
))}
</StyledCardsGrid>
<StyledContentContainer>
{featuredApplications.length > 0 && (
<StyledCardsGrid>
{featuredApplications.map((application) => (
<SettingsAvailableApplicationCard
key={application.id}
application={application}
/>
))}
</StyledCardsGrid>
)}
{!showFeaturedOnly && nonFeaturedApplications.length > 0 && (
<StyledNotFeaturedContainer>
<InlineBanner
color={'danger'}
message={t`Applications below are not vetted. Use at your own risk.`}
/>
<StyledCardsGrid>
{nonFeaturedApplications.map((application) => (
<SettingsAvailableApplicationCard
key={application.id}
application={application}
/>
))}
</StyledCardsGrid>
</StyledNotFeaturedContainer>
)}
</StyledContentContainer>
)}
</Section>
);
@@ -1,26 +1,18 @@
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
import {
StyledActionTableCell,
StyledNameTableCell,
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext, useMemo, useState } from 'react';
import { useContext, useState } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { CommandBlock } from 'twenty-ui/data-display';
import { InlineBanner } from 'twenty-ui/feedback';
import { IconArrowUpRight, IconChevronRight, IconCopy } from 'twenty-ui/icon';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { H2Title } from 'twenty-ui/typography';
import { Button, SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
@@ -29,14 +21,10 @@ import {
FindManyApplicationRegistrationsDocument,
} from '~/generated-metadata/graphql';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
import { useMarketplaceApps } from '~/modules/marketplace/hooks/useMarketplaceApps';
import {
APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
SettingsApplicationTableRow,
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
const StyledButtonContainer = styled.div`
display: flex;
@@ -53,43 +41,17 @@ const StyledTableRowsContainer = styled.div`
padding: ${themeCssVariables.spacing[2]} 0;
`;
const NPM_PACKAGES_GRID_COLUMNS = '200px 1fr 36px';
export const SettingsApplicationsDeveloperTab = () => {
const { t } = useLingui();
const { theme } = useContext(ThemeContext);
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
const [displayNotVettedApps, setDisplayNotVettedApps] = useState(false);
const { copyToClipboard } = useCopyToClipboard();
const { data } = useQuery(FindManyApplicationRegistrationsDocument);
const isMarketplaceSettingTabVisible = useIsFeatureEnabled(
FeatureFlagKey.IS_MARKETPLACE_SETTING_TAB_VISIBLE,
);
const [marketplaceAppSearchTerm, setMarketplaceAppSearchTerm] = useState('');
const [myAppsSearchTerm, setMyAppsSearchTerm] = useState('');
const { data: marketplaceApps } = useMarketplaceApps();
const filteredMarketplaceApps = useMemo(() => {
if (!marketplaceAppSearchTerm) {
return marketplaceApps;
}
const lowerSearch = marketplaceAppSearchTerm.toLowerCase();
return marketplaceApps.filter(
(application) =>
application.name.toLowerCase().includes(lowerSearch) ||
application.description.toLowerCase().includes(lowerSearch),
);
}, [marketplaceApps, marketplaceAppSearchTerm]);
const registrations: ApplicationRegistrationListItemFragment[] =
data?.findManyApplicationRegistrations ?? [];
@@ -191,82 +153,6 @@ export const SettingsApplicationsDeveloperTab = () => {
</Table>
</Section>
)}
{!isMarketplaceSettingTabVisible && (
<Section>
<H2Title
title={t`NPM packages`}
description={t`Apps made by other developers published on npm`}
/>
<InlineBanner
color={'danger'}
message={t`These apps are not vetted. Use at your own risk.`}
button={{
title: t`Access`,
hidden: displayNotVettedApps,
onClick: () => setDisplayNotVettedApps(true),
}}
/>
{displayNotVettedApps && (
<>
<StyledSearchInputContainer>
<SearchInput
placeholder={t`Search an application`}
value={marketplaceAppSearchTerm}
onChange={setMarketplaceAppSearchTerm}
/>
</StyledSearchInputContainer>
{filteredMarketplaceApps.length === 0 ? (
<SettingsEmptyPlaceholder>{t`No application found`}</SettingsEmptyPlaceholder>
) : (
<Table>
<TableRow gridAutoColumns={NPM_PACKAGES_GRID_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Description`}</TableHeader>
<TableHeader />
</TableRow>
<StyledTableRowsContainer>
{filteredMarketplaceApps.map((application) => (
<TableRow
key={application.id}
gridAutoColumns={NPM_PACKAGES_GRID_COLUMNS}
to={getSettingsPath(
SettingsPath.AvailableApplicationDetail,
{
availableApplicationId: application.id,
},
)}
>
<StyledNameTableCell>
<ApplicationDisplay application={application} />
</StyledNameTableCell>
<TableCell
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
<OverflowingTextWithTooltip
text={getApplicationDescriptionSummary(
application.description,
)}
/>
</TableCell>
<StyledActionTableCell>
<IconChevronRight
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
color={theme.font.color.light}
/>
</StyledActionTableCell>
</TableRow>
))}
</StyledTableRowsContainer>
</Table>
)}
</>
)}
</Section>
)}
</>
);
};
@@ -0,0 +1,35 @@
import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
const FEATURED_UNIVERSAL_IDENTIFIERS = [
'8da4b8b5-5edf-4880-b51f-ab6e679ec617',
'4a1178c1-3535-4a47-b592-231d3216b36f',
'66a504cc-0a75-410e-a43f-cdeae1db1522'
];
@RegisteredInstanceCommand('2.19.0', 1783120000000, { type: 'slow' })
export class BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand
implements SlowInstanceCommand
{
async runDataMigration(dataSource: DataSource): Promise<void> {
await dataSource.query(
`UPDATE "core"."applicationRegistration"
SET "isFeatured" = true
WHERE "universalIdentifier" = ANY($1::uuid[])`,
[FEATURED_UNIVERSAL_IDENTIFIERS],
);
}
public async up(_queryRunner: QueryRunner): Promise<void> {}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`UPDATE "core"."applicationRegistration"
SET "isFeatured" = false
WHERE "universalIdentifier" = ANY($1::uuid[])`,
[FEATURED_UNIVERSAL_IDENTIFIERS],
);
}
}
@@ -98,6 +98,7 @@ import { AddLogoToApplicationRegistrationFastInstanceCommand } from './2-19/2-19
import { BackfillLogoOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783069673191-backfill-logo-on-application-registration';
import { AddDisplayFieldsToApplicationRegistrationFastInstanceCommand } from './2-19/2-19-instance-command-fast-1783073776590-add-display-fields-to-application-registration';
import { BackfillDisplayFieldsOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783073776591-backfill-display-fields-on-application-registration';
import { BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand } from './2-19/2-19-instance-command-slow-1783120000000-backfill-is-featured-on-application-registration';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -198,4 +199,5 @@ export const INSTANCE_COMMANDS = [
BackfillDisplayFieldsOnApplicationRegistrationSlowInstanceCommand,
AddStatusToFileFastInstanceCommand,
AddPendingMimeCheckToFileFastInstanceCommand,
BackfillIsFeaturedOnApplicationRegistrationSlowInstanceCommand,
];
@@ -1,4 +0,0 @@
export const MARKETPLACE_CURATED_APPLICATIONS: {
universalIdentifier: string;
position?: number;
}[] = [];
@@ -0,0 +1,17 @@
export const MARKETPLACE_FEATURED_APPLICATIONS: {
universalIdentifier: string;
position?: number;
}[] = [
{
universalIdentifier: '8da4b8b5-5edf-4880-b51f-ab6e679ec617',
position: 1,
},
{
universalIdentifier: '4a1178c1-3535-4a47-b592-231d3216b36f',
position: 2,
},
{
universalIdentifier: '66a504cc-0a75-410e-a43f-cdeae1db1522',
position: 3,
},
];
@@ -32,7 +32,7 @@ import { ApplicationEntity } from 'src/engine/core-modules/application/applicati
import { validateRedirectUri } from 'src/engine/core-modules/auth/utils/validate-redirect-uri.util';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
import { MARKETPLACE_CURATED_APPLICATIONS } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-curated-applications.constant';
import { MARKETPLACE_FEATURED_APPLICATIONS } from 'src/engine/core-modules/application/application-marketplace/constants/marketplace-featured-applications.constant';
const BCRYPT_SALT_ROUNDS = 10;
@@ -281,6 +281,7 @@ export class ApplicationRegistrationService {
if (isDefined(update.isListed)) updateData.isListed = update.isListed;
if (isDefined(update.isPreInstalled))
updateData.isPreInstalled = update.isPreInstalled;
if (isDefined(update.isFeatured)) updateData.isFeatured = update.isFeatured;
if (Object.keys(updateData).length > 0) {
await this.applicationRegistrationRepository.update(id, updateData);
@@ -383,13 +384,13 @@ export class ApplicationRegistrationService {
params.universalIdentifier,
);
const curatedIdentifiers = new Set(
MARKETPLACE_CURATED_APPLICATIONS.map(
const featuredIdentifiers = new Set(
MARKETPLACE_FEATURED_APPLICATIONS.map(
(entry) => entry.universalIdentifier,
),
);
const isFeatured = curatedIdentifiers.has(params.universalIdentifier);
const isFeatured = featuredIdentifiers.has(params.universalIdentifier);
if (isDefined(existing)) {
await this.applicationRegistrationRepository.save({
@@ -400,7 +401,6 @@ export class ApplicationRegistrationService {
latestAvailableVersion: params.latestAvailableVersion,
manifest: params.manifest,
...fromManifestApplicationToDisplayFields(params.manifest?.application),
isFeatured,
});
} else {
const registration = this.applicationRegistrationRepository.create({
@@ -46,6 +46,11 @@ export class UpdateApplicationRegistrationPayload {
@IsBoolean()
@IsOptional()
isPreInstalled?: boolean;
@Field(() => Boolean, { nullable: true })
@IsBoolean()
@IsOptional()
isFeatured?: boolean;
}
@InputType()
@@ -236,7 +236,6 @@ describe('WorkspaceEntityManager', () => {
featureFlagsMap: {
IS_UNIQUE_INDEXES_ENABLED: false,
IS_JSON_FILTER_ENABLED: false,
IS_MARKETPLACE_SETTING_TAB_VISIBLE: false,
IS_EMAIL_GROUP_ENABLED: false,
IS_JUNCTION_RELATIONS_ENABLED: false,
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: false,
@@ -35,11 +35,6 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_MARKETPLACE_SETTING_TAB_VISIBLE,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_SETTINGS_DISCOVERY_HERO_ENABLED,
workspaceId: workspaceId,
@@ -1,7 +1,6 @@
export enum FeatureFlagKey {
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_MARKETPLACE_SETTING_TAB_VISIBLE = 'IS_MARKETPLACE_SETTING_TAB_VISIBLE',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',