Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './nuc_api'
export * from './nuc_auth'
export * from './nuc_charts'
export * from './nuc_colors'
export * from './nuc_entities'
export * from './nuc_fields'
export * from './nuc_globals'
export * from './nuc_languages'
Expand All @@ -14,6 +15,9 @@ export * from './nuc_sections'
export * from './nuc_stores'
export * from './nuc_templates'
export * from './nuc_tooltip'
export { NucEntityDataTableCard } from './nuc_datatable'
export { NucDialog } from './nuc_dialog/index.tsx'
export { useNucDialog } from './nuc_dialog/utils/use_nuc_dialog'

export { getEmailUsTextFields } from './nuc_sections/components/email-us/constants/text_fields'
export { submitContactForm } from './nuc_sections/components/email-us/utils/submit_form'
Expand Down
46 changes: 42 additions & 4 deletions modules/nuc_api/utils/api_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ function resolveApiUrl(url: string): string {
return `${baseUrl}${url}`
}

function stripApiPrefix(url: string): string {
return url.startsWith('/api/') ? url.slice(4) : url
}

export async function apiRequest<T>(
url: string,
method: HttpMethodType = 'GET',
Expand Down Expand Up @@ -54,28 +58,62 @@ export async function apiRequest<T>(
const queryString = searchParams.toString()
const requestUrl = queryString ? `${finalApiUrl}?${queryString}` : finalApiUrl

const response = await fetch(requestUrl, {
let response = await fetch(requestUrl, {
method,
body: data ? JSON.stringify(data) : undefined,
headers,
credentials: 'include',
})

// Fallback for environments where backend routes are exposed without `/api`.
if (
!response.ok &&
response.status === 404 &&
!/^https?:\/\//.test(finalUrl)
) {
const fallbackUrl = stripApiPrefix(finalUrl)
if (fallbackUrl !== finalUrl) {
const resolvedFallbackUrl = resolveApiUrl(fallbackUrl)
const fallbackRequestUrl = queryString
? `${resolvedFallbackUrl}?${queryString}`
: resolvedFallbackUrl

response = await fetch(fallbackRequestUrl, {
method,
body: data ? JSON.stringify(data) : undefined,
headers,
credentials: 'include',
})
}
}

if (!response.ok) {
let errorData: unknown = null
try {
errorData = await response.json()
} catch {
errorData = { error: response.statusText }
}

throw {
const responseData =
errorData && typeof errorData === 'object'
? (errorData as { error?: string; errors?: string })
: null
const message =
responseData?.error ||
responseData?.errors ||
response.statusText ||
`Request failed with status ${response.status}`
const requestError = new Error(message)

Object.assign(requestError, {
response: {
status: response.status,
data: errorData,
},
data: errorData,
}
})

throw requestError
}

return (await response.json()) as ApiResponseType<T>
Expand Down
7 changes: 3 additions & 4 deletions modules/nuc_api/utils/use_api_errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,20 @@ export function useApiErrors(): UseApiErrorsInterface {
flashToast('An unknown error occurred', 'error')
}

throw error
return
}

if (error instanceof Error) {
flashToast(error.message, 'error')
throw error
return
}

if (typeof error === 'string') {
flashToast(error, 'error')
throw new Error(error)
return
}

flashToast('An unknown error occurred', 'error')
throw new Error('An unknown error occurred')
}

return {
Expand Down
9 changes: 7 additions & 2 deletions modules/nuc_charts/atomic/template/entity-chart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'
import type { ChartData } from 'chart.js'
import React, { useEffect, useMemo, useState } from 'react'
import React, { useEffect, useMemo, useRef, useState } from 'react'

import { AdChart, type ChartType } from 'nucleify'

Expand All @@ -11,6 +11,7 @@ export const NucEntityChart: React.FC<NucEntityChartInterface> = (props) => {
const { setChartData, setChartOptions } = useChart()

const [chartData, setChartDataState] = useState<ChartData | null>(null)
const lastDataKeyRef = useRef<string>('')

const chartOptions = useMemo(() => {
if (!props.type) return {}
Expand All @@ -34,7 +35,11 @@ export const NucEntityChart: React.FC<NucEntityChartInterface> = (props) => {
)

if (dataToSet) {
setChartDataState(dataToSet)
const dataKey = JSON.stringify(dataToSet)
if (dataKey !== lastDataKeyRef.current) {
lastDataKeyRef.current = dataKey
setChartDataState(dataToSet)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from 'primereact/button'
import { Card } from 'primereact/card'
import type { DataTableFilterMeta } from 'primereact/datatable'
import { Skeleton } from 'primereact/skeleton'
import React, { useMemo, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'

import { NucEntityDataTable } from '../entity-datatable'

Expand All @@ -19,6 +19,17 @@ export const NucEntityDataTableCard: React.FC<

const [shareDialogVisible, setShareDialogVisible] = useState(false)
const [selectedItems, setSelectedItems] = useState<unknown[]>([])
const handleSelectedUpdate = useCallback((selected: unknown[]) => {
setSelectedItems((prev) => {
if (
prev.length === selected.length &&
prev.every((item, index) => item === selected[index])
) {
return prev
}
return selected
})
}, [])
type ColumnWithField = { field?: string }

const specificColumns = useMemo(() => {
Expand Down Expand Up @@ -124,7 +135,7 @@ export const NucEntityDataTableCard: React.FC<
totalRecords: '{totalRecords}',
})}
selection={selectedItems}
onSelectedUpdate={(selected) => setSelectedItems(selected)}
onSelectedUpdate={handleSelectedUpdate}
/>
)}
</Card>
Expand Down
55 changes: 26 additions & 29 deletions modules/nuc_dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export function NucDialog(props: NucDialogInterface) {
{...rest}
modal={props.modal ?? true}
showHeader={props.showHeader ?? true}
onHide={() => {
if (action) close?.(action)
}}
className={`nuc-dialog ${action || ''}`}
header={
action === 'show' && selectedObject ? (
Expand Down Expand Up @@ -117,13 +120,16 @@ export function NucDialog(props: NucDialogInterface) {
const FieldComponent = getComponent(
field.type as ComponentType
) as React.ElementType
const isSelectLike = isSelectOrDatePicker(field.type)
const isPasswordConfirmation =
field.name === 'password_confirmation'
return (
<div key={index} className="form-div">
<label htmlFor={field.name}>{t(field.label)}</label>
<FieldComponent
{...translatedProps(field.props)}
id={field.name}
value={formData[field.name]}
value={formData[field.name] ?? ''}
onChange={(e: {
target?: { value: unknown }
value?: unknown
Expand All @@ -136,34 +142,25 @@ export function NucDialog(props: NucDialogInterface) {
}))
}
adType={entity as AdTypeType}
panelClass={
isSelectOrDatePicker(field.type) ? entity : undefined
}
dateFormat={
field.type === 'date-picker' ? 'yy-mm-dd' : undefined
}
toggleMask={field.type === 'password' ? true : undefined}
passwordsMatch={
field.name === 'password_confirmation' &&
passwordsMatch(
formData.password,
formData.password_confirmation
)
? true
: undefined
}
emptyPassword={
field.name === 'password_confirmation' &&
isEmpty(formData.password)
? true
: undefined
}
emptyConfirmPassword={
field.name === 'password_confirmation' &&
isEmpty(formData.password_confirmation)
? true
: undefined
}
{...(isSelectLike ? { panelClass: entity } : {})}
{...(field.type === 'date-picker'
? { dateFormat: 'yy-mm-dd' }
: {})}
{...(field.type === 'password' ? { toggleMask: true } : {})}
{...(isPasswordConfirmation &&
passwordsMatch(
formData.password,
formData.password_confirmation
)
? { passwordsMatch: true }
: {})}
{...(isPasswordConfirmation && isEmpty(formData.password)
? { emptyPassword: true }
: {})}
{...(isPasswordConfirmation &&
isEmpty(formData.password_confirmation)
? { emptyConfirmPassword: true }
: {})}
mask={isPhoneField(field.name) ? '999-999-999' : undefined}
placeholder={isPhoneField(field.name) ? '999-999-999' : ''}
unmask={isPhoneField(field.name) ? true : undefined}
Expand Down
11 changes: 11 additions & 0 deletions modules/nuc_entities/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# <img src="https://nucleify.io/favicon.ico" width="17" height="17" /> &nbsp; nuc_entities

Module for all classic entities in Nucleify.

<br>

<h2> &nbsp; <img src="https://nucleify.io/img/technologies/github.svg" width="25"> &nbsp; Contributors </h2> <br>

<a href="https://github.com/SzymCode" target="_blank"><img src="https://nucleify.io/img/contributors/szymcode.svg" width="30" height="30" /></a>
<a href="https://github.com/kbloski" target="_blank"><img src="https://nucleify.io/img/contributors/kbloski.svg" width="30" height="30" /></a>
<a href="https://github.com/kbujak09" target="_blank"><img src="https://nucleify.io/img/contributors/kbujak09.svg" width="30" height="30" /></a>
34 changes: 34 additions & 0 deletions modules/nuc_entities/atomic/bosons/constants/fields/article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { EntityFieldInterface, UseFieldsInterface } from 'nucleify'

export function useArticleFields(): UseFieldsInterface<EntityFieldInterface> {
const fieldData: readonly [string, string, string][] = [
['title', 'field-title', 'input-text'],
['description', 'field-description', 'textarea'],
['category', 'field-category', 'input-text'],
['updated_at', 'field-updated-at', ''],
['created_at', 'field-created-at', ''],
] as const

const createAndEditFields: readonly EntityFieldInterface[] = fieldData
.filter(([name]) => !['created_at', 'updated_at'].includes(name))
.map(
([name, label, type]): EntityFieldInterface => ({
name,
label,
type,
})
)

const showFields: readonly { label: string; key: string }[] = fieldData.map(
([key, label]) => ({
name: key,
key,
label,
})
)

return {
createAndEditFields,
showFields,
}
}
42 changes: 42 additions & 0 deletions modules/nuc_entities/atomic/bosons/constants/fields/contact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { EntityFieldInterface, UseFieldsInterface } from 'nucleify'
import { roles } from 'nucleify'

export function useContactFields(): UseFieldsInterface<EntityFieldInterface> {
const fieldData: readonly [string, string, string][] = [
['first_name', 'field-first-name', 'input-text'],
['last_name', 'field-last-name', 'input-text'],
['email', 'field-email', 'input-text'],
['personal_phone', 'field-personal-phone', 'input-mask'],
['work_phone', 'field-work-phone', 'input-mask'],
['address', 'field-address', 'textarea'],
['birthday', 'field-birthday', 'date-picker'],
['contact_groups', 'field-contact-groups', 'input-text'],
['role', 'field-role', 'select'],
['updated_at', 'field-updated-at', ''],
['created_at', 'field-created-at', ''],
] as const

const createAndEditFields: EntityFieldInterface[] = fieldData
.filter(([name]) => !['created_at', 'updated_at'].includes(name))
.map(([name, label, type]): EntityFieldInterface => {
const props =
name === 'role'
? { options: roles, placeholder: 'field-select-role' }
: undefined

return { name, label, type, props }
})

const showFields: readonly { label: string; key: string }[] = fieldData.map(
([key, label]) => ({
name: key,
key,
label,
})
)

return {
createAndEditFields,
showFields,
}
}
3 changes: 3 additions & 0 deletions modules/nuc_entities/atomic/bosons/constants/fields/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './article'
export * from './contact'
export * from './money'
Loading